From 18f2a20bdcbd915a37dc8c49e2f88069d7506dfb 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 | 47 ++++------ .../steps/ConfigGenerationBuildStep.java | 33 ++++--- .../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 ++ 16 files changed, 387 insertions(+), 70 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 ee5ebd04d064db..6c2018d95ca766 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 @@ -160,6 +160,8 @@ public final class RunTimeConfigurationGenerator { IntFunction.class); static final MethodDescriptor CU_CONFIG_BUILDER = MethodDescriptor.ofMethod(ConfigUtils.class, "configBuilder", SmallRyeConfigBuilder.class, boolean.class, LaunchMode.class); + static final MethodDescriptor CU_LOAD_RUNTIME_DEFAULT_VALUES = MethodDescriptor.ofMethod(ConfigUtils.class, + "loadRuntimeDefaultValues", void.class, Map.class); static final MethodDescriptor CU_CONFIG_BUILDER_WITH_ADD_DISCOVERED = MethodDescriptor.ofMethod(ConfigUtils.class, "configBuilder", SmallRyeConfigBuilder.class, boolean.class, boolean.class, LaunchMode.class); @@ -265,7 +267,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; @@ -283,8 +285,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<>(); @@ -317,7 +317,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", @@ -326,7 +326,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(); @@ -343,7 +342,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 { @@ -460,7 +459,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 @@ -540,7 +539,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 @@ -549,17 +548,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 @@ -706,7 +699,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 +719,7 @@ public void run() { } clinit.invokeStaticMethod(initGroup, clinitConfig, clinitNameBuilder, instance); clinit.invokeVirtualMethod(SB_SET_LENGTH, clinitNameBuilder, clInitOldLen); - if (devMode) { + if (liveReloadPossible) { instance = readConfig.readStaticField(rootFieldDescriptor); if (!rootName.isEmpty()) { readConfig.invokeVirtualMethod(SB_APPEND_CHAR, readConfigNameBuilder, readConfig.load('.')); @@ -796,7 +789,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); } // generate sweep for run time @@ -1673,10 +1666,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; @@ -1699,6 +1692,11 @@ public Builder setClassOutput(final ClassOutput classOutput) { return this; } + public Builder setLiveReloadPossible(boolean liveReloadPossible) { + this.liveReloadPossible = liveReloadPossible; + return this; + } + BuildTimeConfigurationReader.ReadResult getBuildTimeReadResult() { return buildTimeReadResult; } @@ -1708,15 +1706,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 0eacb06801099f..6846bd3011b8a4 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,6 +34,7 @@ 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; @@ -46,15 +49,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 +80,24 @@ void staticInitSources( PropertiesLocationConfigSourceFactory.class.getName())); } + @BuildStep + GeneratedResourceBuildItem runtimeDefaultsConfig(List runTimeDefaults) + throws IOException { + Properties p = new Properties(); + for (var e : runTimeDefaults) { + p.setProperty(e.getKey(), e.getValue()); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + p.store(out, null); + 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 +112,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 +130,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)) @@ -157,7 +168,7 @@ public SuppressNonRuntimeConfigChangedWarningBuildItem ignoreQuarkusProfileChang @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 7831fbc703e40d..fbe44bbd7b74c8 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 eb7a8e09bb58f4..2273573e5b68bf 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 c9d2c35e3088c9..acf33674999b93 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.TreeMap; @@ -37,6 +39,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; @@ -52,6 +55,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() { } @@ -117,6 +121,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; } @@ -191,14 +199,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 537dbbec2327c1..f4abd6e982104f 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 @@ -277,6 +277,13 @@ private DevServicesDatasourceResultBuildItem.DbResult startDevDb(String dbName, if (datasource.getPassword() != null) { devDebProperties.put(prefix + "password", datasource.getPassword()); } + + 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); } } diff --git a/extensions/jdbc/jdbc-postgresql/deployment/pom.xml b/extensions/jdbc/jdbc-postgresql/deployment/pom.xml index cbfd96e78561d2..739ecf38927e95 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 00000000000000..a2e6cca4ff2078 --- /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 00000000000000..52a412899eeb4c --- /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 700c5173ce6426..bf674418945b53 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 00000000000000..b49922cc2a616a --- /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 00000000000000..9471f16eca404d --- /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 00000000000000..6a434b84b527a5 --- /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 ee2e615a09fb42..39519f0cdde3e5 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; @@ -1156,6 +1156,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 37cd593f9b9eb0..63f59090c43c08 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) {