From 69b98e513d1604fced1a444f8cb77a2df09e567d Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Wed, 28 Jul 2021 17:14:44 +1000 Subject: [PATCH] RuntimeConfigDefault changes ignored on restart Also makes sure datasources restart if devservices properties are changed. Fixes #17069 Fixes #19931 --- .../RunTimeConfigurationGenerator.java | 45 ++++------ .../steps/ConfigGenerationBuildStep.java | 37 +++++--- .../steps/DevServicesConfigBuildStep.java | 20 ++--- ...hangeRecorder.java => ConfigRecorder.java} | 4 +- .../runtime/configuration/ConfigUtils.java | 32 ++++++- .../DevServicesDatasourceProcessor.java | 7 ++ .../jdbc/jdbc-postgresql/deployment/pom.xml | 10 +++ ...esPostgresqlDatasourceDevModeTestCase.java | 53 +++++++++++ .../postgresql/deployment/PgResource.java | 58 +++++++++++++ .../deployment/MongoClientProcessor.java | 22 ++--- .../dev/KafkaDevServicesDevModeTestCase.java | 87 +++++++++++++++++++ .../kafka/deployment/dev/PriceConverter.java | 20 +++++ .../kafka/deployment/dev/PriceGenerator.java | 22 +++++ .../kafka/deployment/dev/PriceResource.java | 25 ++++++ .../vertx/http/runtime/VertxHttpRecorder.java | 10 ++- .../devmode/VertxHttpHotReplacementSetup.java | 7 ++ .../test/junit/QuarkusTestExtension.java | 7 +- 17 files changed, 394 insertions(+), 72 deletions(-) rename core/runtime/src/main/java/io/quarkus/runtime/configuration/{ConfigChangeRecorder.java => ConfigRecorder.java} (94%) create mode 100644 extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceDevModeTestCase.java create mode 100644 extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/PgResource.java create mode 100644 extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/KafkaDevServicesDevModeTestCase.java create mode 100644 extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceConverter.java create mode 100644 extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceGenerator.java create mode 100644 extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceResource.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index 1594f0ab9fc05..a519468024707 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -273,7 +273,7 @@ private RunTimeConfigurationGenerator() { } public static final class GenerateOperation implements AutoCloseable { - final boolean devMode; + final boolean liveReloadPossible; final LaunchMode launchMode; final AccessorFinder accessorFinder; final ClassOutput classOutput; @@ -291,8 +291,6 @@ public static final class GenerateOperation implements AutoCloseable { // default values given in the build configuration final Map specifiedRunTimeDefaultValues; final Map buildTimeRunTimeVisibleValues; - // default values produced by extensions via build item - final Map runTimeDefaults; final Map enclosingMemberMethods = new HashMap<>(); final Map, MethodDescriptor> groupInitMethods = new HashMap<>(); final Map, FieldDescriptor> configRootsByType = new HashMap<>(); @@ -325,7 +323,7 @@ public static final class GenerateOperation implements AutoCloseable { GenerateOperation(Builder builder) { this.launchMode = builder.launchMode; - this.devMode = builder.launchMode == LaunchMode.DEVELOPMENT; + this.liveReloadPossible = builder.liveReloadPossible; final BuildTimeConfigurationReader.ReadResult buildTimeReadResult = builder.buildTimeReadResult; buildTimeConfigResult = Assert.checkNotNullParam("buildTimeReadResult", buildTimeReadResult); specifiedRunTimeDefaultValues = Assert.checkNotNullParam("specifiedRunTimeDefaultValues", @@ -334,7 +332,6 @@ public static final class GenerateOperation implements AutoCloseable { buildTimeReadResult.getBuildTimeRunTimeVisibleValues()); classOutput = Assert.checkNotNullParam("classOutput", builder.getClassOutput()); roots = Assert.checkNotNullParam("builder.roots", builder.getBuildTimeReadResult().getAllRoots()); - runTimeDefaults = Assert.checkNotNullParam("runTimeDefaults", builder.getRunTimeDefaults()); additionalTypes = Assert.checkNotNullParam("additionalTypes", builder.getAdditionalTypes()); additionalBootstrapConfigSourceProviders = builder.getAdditionalBootstrapConfigSourceProviders(); staticConfigSources = builder.getStaticConfigSources(); @@ -351,7 +348,7 @@ public static final class GenerateOperation implements AutoCloseable { mc.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), mc.getThis()); mc.returnValue(null); } - if (devMode) { + if (liveReloadPossible) { reinit = cc.getMethodCreator(REINIT); reinit.setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC); } else { @@ -464,7 +461,7 @@ public void run() { // make the build time config global until we read the run time config - // at run time (when we're ready) we update the factory and then release the build time config installConfiguration(clinitConfig, clinit); - if (devMode) { + if (liveReloadPossible) { final ResultHandle buildTimeRunTimeDefaultValuesConfigSource = reinit .readStaticField(C_BUILD_TIME_RUN_TIME_DEFAULTS_CONFIG_SOURCE); // create the map for build time config source @@ -544,7 +541,7 @@ public void run() { // create the map for run time specified values config source final ResultHandle specifiedRunTimeValues = clinit.newInstance(HM_NEW); - if (!devMode) { + if (!liveReloadPossible) { //we don't need these in devmode //including it would just cache the first values //but these can already just be read directly, as we are in the same JVM @@ -553,17 +550,11 @@ public void run() { clinit.load(entry.getValue())); } } - for (Map.Entry entry : runTimeDefaults.entrySet()) { - if (!specifiedRunTimeDefaultValues.containsKey(entry.getKey())) { - // only add entry if the user didn't override it - clinit.invokeVirtualMethod(HM_PUT, specifiedRunTimeValues, clinit.load(entry.getKey()), - clinit.load(entry.getValue())); - } - } final ResultHandle specifiedRunTimeSource = clinit.newInstance(PCS_NEW, specifiedRunTimeValues, clinit.load("Specified default values"), clinit.load(Integer.MIN_VALUE + 100)); + cc.getFieldCreator(C_SPECIFIED_RUN_TIME_CONFIG_SOURCE) - .setModifiers(Opcodes.ACC_STATIC | (devMode ? Opcodes.ACC_VOLATILE : Opcodes.ACC_FINAL)); + .setModifiers(Opcodes.ACC_STATIC | (liveReloadPossible ? Opcodes.ACC_VOLATILE : Opcodes.ACC_FINAL)); clinit.writeStaticField(C_SPECIFIED_RUN_TIME_CONFIG_SOURCE, specifiedRunTimeSource); // add in the custom sources that bootstrap config needs @@ -709,7 +700,7 @@ public void run() { // config root field is volatile in dev mode, final otherwise; we initialize it from clinit, and readConfig in dev mode cc.getFieldCreator(rootFieldDescriptor) .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC - | (devMode ? Opcodes.ACC_VOLATILE : Opcodes.ACC_FINAL)); + | (liveReloadPossible ? Opcodes.ACC_VOLATILE : Opcodes.ACC_FINAL)); // construct instance in ResultHandle instance; @@ -726,7 +717,7 @@ public void run() { clinit.invokeVirtualMethod(SB_APPEND_STRING, clinitNameBuilder, clinit.load(root.getName())); clinit.invokeStaticMethod(initGroup, clinitConfig, clinitNameBuilder, instance); clinit.invokeVirtualMethod(SB_SET_LENGTH, clinitNameBuilder, clInitOldLen); - if (devMode) { + if (liveReloadPossible) { instance = readConfig.readStaticField(rootFieldDescriptor); readConfig.invokeVirtualMethod(SB_APPEND_STRING, readConfigNameBuilder, readConfig.load(root.getName())); @@ -785,7 +776,7 @@ public void run() { clinit.invokeStaticMethod(CD_UNKNOWN_PROPERTIES, clinit.readStaticField(FieldDescriptor.of(cc.getClassName(), "unused", List.class))); - if (devMode) { + if (liveReloadPossible) { configSweepLoop(siParserBody, readConfig, runTimeConfig, getRegisteredRoots(RUN_TIME)); } // generate sweep for run time @@ -1672,10 +1663,10 @@ public static Builder builder() { } public static final class Builder { + public boolean liveReloadPossible; private LaunchMode launchMode; private ClassOutput classOutput; private BuildTimeConfigurationReader.ReadResult buildTimeReadResult; - private Map runTimeDefaults; private List> additionalTypes; private List additionalBootstrapConfigSourceProviders; @@ -1698,6 +1689,11 @@ public Builder setClassOutput(final ClassOutput classOutput) { return this; } + public Builder setLiveReloadPossible(boolean liveReloadPossible) { + this.liveReloadPossible = liveReloadPossible; + return this; + } + BuildTimeConfigurationReader.ReadResult getBuildTimeReadResult() { return buildTimeReadResult; } @@ -1707,15 +1703,6 @@ public Builder setBuildTimeReadResult(final BuildTimeConfigurationReader.ReadRes return this; } - Map getRunTimeDefaults() { - return runTimeDefaults; - } - - public Builder setRunTimeDefaults(final Map runTimeDefaults) { - this.runTimeDefaults = runTimeDefaults; - return this; - } - List> getAdditionalTypes() { return additionalTypes; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 90675727b9619..4efa12878680f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -4,6 +4,7 @@ import static io.quarkus.deployment.util.ServiceUtil.classNamesNamedIn; import static java.util.stream.Collectors.toList; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -13,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; @@ -32,12 +34,14 @@ import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.StaticInitConfigSourceFactoryBuildItem; import io.quarkus.deployment.builditem.StaticInitConfigSourceProviderBuildItem; import io.quarkus.deployment.builditem.SuppressNonRuntimeConfigChangedWarningBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.configuration.BuildTimeConfigurationReader; import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; @@ -46,15 +50,18 @@ import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; +import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.StaticInitSafe; -import io.quarkus.runtime.configuration.ConfigChangeRecorder; +import io.quarkus.runtime.configuration.ConfigRecorder; +import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.ConfigurationRuntimeConfig; import io.quarkus.runtime.configuration.RuntimeOverrideConfigSource; import io.smallrye.config.ConfigSourceFactory; import io.smallrye.config.PropertiesLocationConfigSourceFactory; public class ConfigGenerationBuildStep { + @BuildStep void deprecatedStaticInitBuildItem( List additionalStaticInitConfigSourceProviders, @@ -74,13 +81,27 @@ void staticInitSources( PropertiesLocationConfigSourceFactory.class.getName())); } + @BuildStep + GeneratedResourceBuildItem runtimeDefaultsConfig(List runTimeDefaults, + BuildProducer nativeImageResourceBuildItemBuildProducer) + throws IOException { + Properties p = new Properties(); + for (var e : runTimeDefaults) { + p.setProperty(e.getKey(), e.getValue()); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + p.store(out, null); + nativeImageResourceBuildItemBuildProducer + .produce(new NativeImageResourceBuildItem(ConfigUtils.QUARKUS_RUNTIME_CONFIG_DEFAULTS_PROPERTIES)); + return new GeneratedResourceBuildItem(ConfigUtils.QUARKUS_RUNTIME_CONFIG_DEFAULTS_PROPERTIES, out.toByteArray()); + } + /** * Generate the Config class that instantiates MP Config and holds all the config objects */ @BuildStep void generateConfigClass( ConfigurationBuildItem configItem, - List runTimeDefaults, List typeItems, LaunchModeBuildItem launchModeBuildItem, BuildProducer generatedClass, @@ -95,13 +116,6 @@ void generateConfigClass( return; } - Map defaults = new HashMap<>(); - for (RunTimeConfigurationDefaultBuildItem item : runTimeDefaults) { - if (defaults.putIfAbsent(item.getKey(), item.getValue()) != null) { - throw new IllegalStateException("More than one default value for " + item.getKey() + " was produced"); - } - } - Set discoveredConfigSources = discoverService(ConfigSource.class, reflectiveClass); Set discoveredConfigSourceProviders = discoverService(ConfigSourceProvider.class, reflectiveClass); Set discoveredConfigSourceFactories = discoverService(ConfigSourceFactory.class, reflectiveClass); @@ -120,7 +134,8 @@ void generateConfigClass( .setBuildTimeReadResult(configItem.getReadResult()) .setClassOutput(new GeneratedClassGizmoAdaptor(generatedClass, false)) .setLaunchMode(launchModeBuildItem.getLaunchMode()) - .setRunTimeDefaults(defaults) + .setLiveReloadPossible(launchModeBuildItem.getLaunchMode() == LaunchMode.DEVELOPMENT + || launchModeBuildItem.isAuxiliaryApplication()) .setAdditionalTypes(typeItems.stream().map(ConfigurationTypeBuildItem::getValueType).collect(toList())) .setAdditionalBootstrapConfigSourceProviders( getAdditionalBootstrapConfigSourceProviders(additionalBootstrapConfigSourceProviders)) @@ -161,7 +176,7 @@ public void suppressNonRuntimeConfigChanged( @BuildStep @Record(ExecutionTime.RUNTIME_INIT) public void checkForBuildTimeConfigChange( - ConfigChangeRecorder recorder, ConfigurationBuildItem configItem, LoggingSetupBuildItem loggingSetupBuildItem, + ConfigRecorder recorder, ConfigurationBuildItem configItem, LoggingSetupBuildItem loggingSetupBuildItem, ConfigurationRuntimeConfig configurationConfig, List suppressNonRuntimeConfigChangedWarningItems) { BuildTimeConfigurationReader.ReadResult readResult = configItem.getReadResult(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/DevServicesConfigBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/DevServicesConfigBuildStep.java index 7831fbc703e40..fbe44bbd7b74c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/DevServicesConfigBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/DevServicesConfigBuildStep.java @@ -15,11 +15,11 @@ import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesNativeConfigResultBuildItem; -import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; class DevServicesConfigBuildStep { + static volatile Map oldConfig; @BuildStep List deprecated(List items) { @@ -30,18 +30,16 @@ List deprecated(List runtimeConfig, - List devServicesConfigResultBuildItems, - LiveReloadBuildItem liveReloadBuildItem) { + List devServicesConfigResultBuildItems) { Map newProperties = new HashMap<>(devServicesConfigResultBuildItems.stream().collect( Collectors.toMap(DevServicesConfigResultBuildItem::getKey, DevServicesConfigResultBuildItem::getValue))); Config config = ConfigProvider.getConfig(); - PreviousConfig oldProperties = liveReloadBuildItem.getContextObject(PreviousConfig.class); //check if there are existing already started dev services //if there were no changes to the processors they don't produce config //so we merge existing config from previous runs //we also check the current config, as the dev service may have been disabled by explicit config - if (oldProperties != null) { - for (Map.Entry entry : oldProperties.config.entrySet()) { + if (oldConfig != null) { + for (Map.Entry entry : oldConfig.entrySet()) { if (!newProperties.containsKey(entry.getKey()) && config.getOptionalValue(entry.getKey(), String.class).isEmpty()) { newProperties.put(entry.getKey(), entry.getValue()); @@ -51,15 +49,7 @@ DevServicesLauncherConfigResultBuildItem setup(BuildProducer entry : newProperties.entrySet()) { runtimeConfig.produce(new RunTimeConfigurationDefaultBuildItem(entry.getKey(), entry.getValue())); } - liveReloadBuildItem.setContextObject(PreviousConfig.class, new PreviousConfig(newProperties)); + oldConfig = newProperties; return new DevServicesLauncherConfigResultBuildItem(Collections.unmodifiableMap(newProperties)); } - - static class PreviousConfig { - final Map config; - - public PreviousConfig(Map config) { - this.config = config; - } - } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigChangeRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java similarity index 94% rename from core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigChangeRecorder.java rename to core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java index eb7a8e09bb58f..2273573e5b68b 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigChangeRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java @@ -14,9 +14,9 @@ import io.quarkus.runtime.configuration.ConfigurationRuntimeConfig.BuildTimeMismatchAtRuntime; @Recorder -public class ConfigChangeRecorder { +public class ConfigRecorder { - private static final Logger log = Logger.getLogger(ConfigChangeRecorder.class); + private static final Logger log = Logger.getLogger(ConfigRecorder.class); public void handleConfigChange(ConfigurationRuntimeConfig configurationConfig, Map buildTimeConfig) { Config configProvider = ConfigProvider.getConfig(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index 315ebae2d7c17..125d8e2b8a610 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -8,6 +8,7 @@ import static io.smallrye.config.SmallRyeConfigBuilder.META_INF_MICROPROFILE_CONFIG_PROPERTIES; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -17,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.OptionalInt; +import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -35,6 +37,7 @@ import io.smallrye.config.EnvConfigSource; import io.smallrye.config.FallbackConfigSourceInterceptor; import io.smallrye.config.Priorities; +import io.smallrye.config.PropertiesConfigSource; import io.smallrye.config.RelocateConfigSourceInterceptor; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -50,6 +53,7 @@ public final class ConfigUtils { * The name of the property associated with a random UUID generated at launch time. */ static final String UUID_KEY = "quarkus.uuid"; + public static final String QUARKUS_RUNTIME_CONFIG_DEFAULTS_PROPERTIES = "quarkus-runtime-config-defaults.properties"; private ConfigUtils() { } @@ -115,6 +119,10 @@ public static SmallRyeConfigBuilder configBuilder(final boolean runTime, final b if (addDiscovered) { builder.addDiscoveredSources(); } + if (runTime || bootstrap) { + Map runtimeDefaults = loadRuntimeDefaultValues(); + builder.withSources(new PropertiesConfigSource(runtimeDefaults, "Runtime Defaults", Integer.MIN_VALUE + 50)); + } return builder; } @@ -189,14 +197,34 @@ public static void addSourceFactoryProvider(SmallRyeConfigBuilder builder, Confi builder.withSources(provider.getConfigSourceFactory(Thread.currentThread().getContextClassLoader())); } + public static Map loadRuntimeDefaultValues() { + Map values = new HashMap<>(); + try (InputStream in = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(QUARKUS_RUNTIME_CONFIG_DEFAULTS_PROPERTIES)) { + if (in == null) { + return values; + } + Properties p = new Properties(); + p.load(in); + for (String k : p.stringPropertyNames()) { + if (!values.containsKey(k)) { + values.put(k, p.getProperty(k)); + } + } + return values; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Checks if a property is present in the current Configuration. - * + *

* Because the sources may not expose the property directly in {@link ConfigSource#getPropertyNames()}, we cannot * reliable determine if the property is present in the properties list. The property needs to be retrieved to make * sure it exists. Also, if the value is an expression, we want to ignore expansion, because this is not relevant * for the check and the expansion value may not be available at this point. - * + *

* It may be interesting to expose such API in SmallRyeConfig directly. * * @param propertyName the property name. diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index b23497cbd9024..09b6e30a694f3 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -292,6 +292,13 @@ private DevServicesDatasourceResultBuildItem.DbResult startDevDb(String dbName, compressor.close(); log.info("Dev Services for " + prettyName + " (" + defaultDbKind.get() + ") started."); + + String devservices = prefix + "devservices."; + for (var name : ConfigProvider.getConfig().getPropertyNames()) { + if (name.startsWith(devservices)) { + devDebProperties.put(name, ConfigProvider.getConfig().getValue(name, String.class)); + } + } return new DevServicesDatasourceResultBuildItem.DbResult(defaultDbKind.get(), devDebProperties); } catch (Throwable t) { compressor.closeAndDumpCaptured(); diff --git a/extensions/jdbc/jdbc-postgresql/deployment/pom.xml b/extensions/jdbc/jdbc-postgresql/deployment/pom.xml index cbfd96e78561d..739ecf38927e9 100644 --- a/extensions/jdbc/jdbc-postgresql/deployment/pom.xml +++ b/extensions/jdbc/jdbc-postgresql/deployment/pom.xml @@ -48,6 +48,16 @@ assertj-core test + + io.quarkus + quarkus-resteasy-reactive-deployment + test + + + io.rest-assured + rest-assured + test + diff --git a/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceDevModeTestCase.java b/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceDevModeTestCase.java new file mode 100644 index 0000000000000..a2e6cca4ff207 --- /dev/null +++ b/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceDevModeTestCase.java @@ -0,0 +1,53 @@ +package io.quarkus.jdbc.postgresql.deployment; + +import java.util.logging.Level; + +import javax.inject.Inject; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevServicesPostgresqlDatasourceDevModeTestCase { + + @RegisterExtension + static QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(PgResource.class) + .addAsResource(new StringAsset(""), "application.properties")) + // Expect no warnings (in particular from Agroal) + .setLogRecordPredicate(record -> record.getLevel().intValue() >= Level.WARNING.intValue() + // There are other warnings: JDK8, TestContainers, drivers, ... + // Ignore them: we're only interested in Agroal here. + && record.getMessage().contains("Agroal")); + + @Inject + AgroalDataSource dataSource; + + @Test + public void testDatasource() throws Exception { + RestAssured.get("/pg/save?name=foo&value=bar") + .then().statusCode(204); + + RestAssured.get("/pg/get?name=foo") + .then().statusCode(200) + .body(Matchers.equalTo("bar")); + + test.modifyResourceFile("application.properties", s -> "quarkus.datasource.devservices.properties.log=TRACE"); + + RestAssured.get("/pg/get?name=foo") + .then().statusCode(404); + RestAssured.get("/pg/save?name=foo&value=bar") + .then().statusCode(204); + RestAssured.get("/pg/get?name=foo") + .then().statusCode(200) + .body(Matchers.equalTo("bar")); + } +} diff --git a/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/PgResource.java b/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/PgResource.java new file mode 100644 index 0000000000000..52a412899eeb4 --- /dev/null +++ b/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/PgResource.java @@ -0,0 +1,58 @@ +package io.quarkus.jdbc.postgresql.deployment; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.transaction.Transactional; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import io.agroal.api.AgroalDataSource; +import io.smallrye.common.annotation.Blocking; + +@Path("/pg") +@Blocking +public class PgResource { + + @Inject + AgroalDataSource ds; + + @PostConstruct + @Transactional + public void setup() throws Exception { + try (var con = ds.getConnection()) { + try (var smt = con.createStatement()) { + smt.executeUpdate("create table foo (name varchar(100) primary key not null, value varchar(100));"); + } + } + } + + @GET + @Path("save") + public void save(@QueryParam("name") String name, @QueryParam("value") String value) throws Exception { + try (var con = ds.getConnection()) { + try (var smt = con.prepareStatement("insert into foo (name, value) values (?,?)")) { + smt.setString(1, name); + smt.setString(2, value); + smt.execute(); + } + } + } + + @GET + @Path("get") + public String get(@QueryParam("name") String name) throws Exception { + try (var con = ds.getConnection()) { + try (var smt = con.prepareStatement("select (value) from foo where name = ?")) { + smt.setString(1, name); + try (var rs = smt.executeQuery()) { + if (!rs.next()) { + throw new NotFoundException(); + } + return rs.getString(1); + } + } + } + } +} diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java index 700c5173ce642..bf674418945b5 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java @@ -30,7 +30,6 @@ import com.mongodb.event.ConnectionPoolListener; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; @@ -149,7 +148,7 @@ List addExtensionPointsToNative(CodecProviderBuildItem } @BuildStep - public void mongoClientNames(BeanArchiveIndexBuildItem indexBuildItem, + public void mongoClientNames(CombinedIndexBuildItem indexBuildItem, BuildProducer mongoClientName) { Set values = new HashSet<>(); IndexView indexView = indexBuildItem.getIndex(); @@ -203,10 +202,19 @@ void additionalBeans(BuildProducer additionalBeans) { additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClasses(MongoClients.class).setUnremovable().build()); } + @BuildStep + void connectionNames( + List mongoClientNames, + BuildProducer mongoConnections) { + mongoConnections.produce(new MongoConnectionNameBuildItem(MongoClientBeanUtil.DEFAULT_MONGOCLIENT_NAME)); + for (MongoClientNameBuildItem bi : mongoClientNames) { + mongoConnections.produce(new MongoConnectionNameBuildItem(bi.getName())); + } + } + @Record(STATIC_INIT) @BuildStep void build( - List mongoClientNames, MongoClientRecorder recorder, SslNativeConfigBuildItem sslNativeConfig, CodecProviderBuildItem codecProvider, @@ -214,7 +222,6 @@ void build( BsonDiscriminatorBuildItem bsonDiscriminator, CommandListenerBuildItem commandListener, List connectionPoolListenerProvider, - BuildProducer mongoConnections, BuildProducer syntheticBeanBuildItemBuildProducer) { List> poolListenerList = new ArrayList<>(connectionPoolListenerProvider.size()); @@ -230,11 +237,6 @@ void build( bsonDiscriminator.getBsonDiscriminatorClassNames(), commandListener.getCommandListenerClassNames(), poolListenerList, sslNativeConfig.isExplicitlyDisabled())) .done()); - - mongoConnections.produce(new MongoConnectionNameBuildItem(MongoClientBeanUtil.DEFAULT_MONGOCLIENT_NAME)); - for (MongoClientNameBuildItem bi : mongoClientNames) { - mongoConnections.produce(new MongoConnectionNameBuildItem(bi.getName())); - } } @Record(ExecutionTime.RUNTIME_INIT) @@ -384,4 +386,4 @@ void registerServiceBinding(Capabilities capabilities, BuildProducer() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(PriceConverter.class, PriceResource.class, PriceGenerator.class) + .addAsResource(new StringAsset(FINAL_APP_PROPERTIES), + "application.properties"); + } + }); + + @TestHTTPResource("/prices/stream") + URI uri; + + @Test + public void sseStream() { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(this.uri); + + List received = new CopyOnWriteArrayList<>(); + + try (SseEventSource source = SseEventSource.target(target).build()) { + source.register(inboundSseEvent -> received.add(Double.valueOf(inboundSseEvent.readData()))); + source.open(); + + Awaitility.await() + .atMost(Duration.ofSeconds(1)) + .until(() -> received.size() >= 2); + } + + Assertions.assertThat(received) + .hasSizeGreaterThanOrEqualTo(2) + .allMatch(value -> (value >= 0) && (value < 100)); + + test.modifySourceFile(PriceConverter.class, s -> s.replace("int ", "long ")); + test.modifySourceFile(PriceGenerator.class, + s -> s.replace("Integer", "Long").replace("this.random", "(long)this.random")); + + received.clear(); + + try (SseEventSource source = SseEventSource.target(target).build()) { + source.register(inboundSseEvent -> received.add(Double.valueOf(inboundSseEvent.readData()))); + source.open(); + + Awaitility.await() + .atMost(Duration.ofSeconds(3)) + .until(() -> received.size() >= 2); + } + + Assertions.assertThat(received) + .hasSizeGreaterThanOrEqualTo(2) + .allMatch(value -> (value >= 0) && (value < 100)); + } +} diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceConverter.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceConverter.java new file mode 100644 index 0000000000000..b49922cc2a616 --- /dev/null +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceConverter.java @@ -0,0 +1,20 @@ +package io.quarkus.smallrye.reactivemessaging.kafka.deployment.dev; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.reactive.messaging.annotations.Broadcast; + +@ApplicationScoped +public class PriceConverter { + private static final double CONVERSION_RATE = 0.88; + + @Incoming("prices") + @Outgoing("processed-prices") + @Broadcast + public double process(int priceInUsd) { + return priceInUsd * CONVERSION_RATE; + } +} diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceGenerator.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceGenerator.java new file mode 100644 index 0000000000000..9471f16eca404 --- /dev/null +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceGenerator.java @@ -0,0 +1,22 @@ +package io.quarkus.smallrye.reactivemessaging.kafka.deployment.dev; + +import java.time.Duration; +import java.util.Random; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PriceGenerator { + private final Random random = new Random(); + + @Outgoing("generated-price") + public Multi generate() { + return Multi.createFrom().ticks().every(Duration.ofMillis(10)) + .onOverflow().drop() + .map(tick -> this.random.nextInt(100)); + } +} diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceResource.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceResource.java new file mode 100644 index 0000000000000..6a434b84b527a --- /dev/null +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/PriceResource.java @@ -0,0 +1,25 @@ +package io.quarkus.smallrye.reactivemessaging.kafka.deployment.dev; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.reactivestreams.Publisher; + +@Path("/prices") +public class PriceResource { + private final Publisher processedPrices; + + public PriceResource(@Channel("processed-prices") Publisher processedPrices) { + this.processedPrices = processedPrices; + } + + @GET + @Path("/stream") + @Produces(MediaType.SERVER_SENT_EVENTS) + public Publisher ssePrices() { + return this.processedPrices; + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 4f351e10fe299..63c32359dcf66 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -122,7 +122,7 @@ public class VertxHttpRecorder { private static volatile Runnable closeTask; - private static volatile Handler rootHandler; + static volatile Handler rootHandler; private static volatile Handler nonApplicationRedirectHandler; @@ -1164,6 +1164,14 @@ public static Handler getRootHandler() { return ACTUAL_ROOT; } + /** + * used in the live reload handler to make sure the application has not been changed by another source (e.g. reactive + * messaging) + */ + public static Object getCurrentApplicationState() { + return rootHandler; + } + public Handler createBodyHandler(HttpConfiguration httpConfiguration) { BodyHandler bodyHandler = BodyHandler.create(); Optional maxBodySize = httpConfiguration.limits.maxBodySize; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/VertxHttpHotReplacementSetup.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/VertxHttpHotReplacementSetup.java index 37cd593f9b9eb..63f59090c43c0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/VertxHttpHotReplacementSetup.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/VertxHttpHotReplacementSetup.java @@ -147,12 +147,19 @@ public void handle(Promise event) { synchronized (this) { if (nextUpdate < System.currentTimeMillis() || hotReplacementContext.isTest()) { nextUpdate = System.currentTimeMillis() + HOT_REPLACEMENT_INTERVAL; + Object currentState = VertxHttpRecorder.getCurrentApplicationState(); try { restart = hotReplacementContext.doScan(true); } catch (Exception e) { event.fail(new IllegalStateException("Unable to perform live reload scanning", e)); return; } + if (currentState != VertxHttpRecorder.getCurrentApplicationState()) { + //its possible a Kafka message or some other source triggered a reload + //so we could wait for the restart (due to the scan lock) + //but then fail to dispatch to the new application + restart = true; + } } } if (hotReplacementContext.getDeploymentProblem() != null) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index de9eeac14d807..f20c536af110c 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -15,6 +15,7 @@ import java.time.temporal.ChronoUnit; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -83,6 +84,7 @@ import io.quarkus.test.common.PropertyTestUtil; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.RestAssuredURLManager; +import io.quarkus.test.common.RestorableSystemProperties; import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.TestScopeManager; @@ -253,8 +255,8 @@ public Thread newThread(Runnable r) { hangTaskKey = hangDetectionExecutor.schedule(hangDetectionTask, hangTimeout.toMillis(), TimeUnit.MILLISECONDS); } ConfigProviderResolver.setInstance(new RunningAppConfigResolver(runningQuarkusApplication)); - - System.setProperty("test.url", TestHTTPResourceManager.getUri(runningQuarkusApplication)); + RestorableSystemProperties restorableSystemProperties = RestorableSystemProperties.setProperties( + Collections.singletonMap("test.url", TestHTTPResourceManager.getUri(runningQuarkusApplication))); Closeable tm = testResourceManager; Closeable shutdownTask = new Closeable() { @@ -275,6 +277,7 @@ public void close() throws IOException { try { tm.close(); } finally { + restorableSystemProperties.close(); GroovyCacheCleaner.clearGroovyCache(); shutdownHangDetection(); }