This is an automated email from the ASF dual-hosted git repository. struberg pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/deltaspike.git
commit 33c3c2483907c2b0974c0ac3288e6b004c9e438b Author: Mark Struberg <[email protected]> AuthorDate: Tue Jun 2 06:10:33 2020 +0200 DELTASPIKE-1402 dynamic reload of PropertyFileConfigSource DeltaSpike can now dynamically reload configuration of property files in a file:// location. The time after which we look for file modification (via lastModified time stamp of the file) can be configured with a 'deltaspike_reload=60' property in seconds. In this case 60 seconds. If this property is not set we check for changes every 300 seconds or 5 Minutes. --- .../core/impl/config/PropertiesConfigSource.java | 4 +- .../core/impl/config/PropertyFileConfigSource.java | 211 ++++++++++++++++++++- .../BaseTestConfigProperty.java | 34 ++++ 3 files changed, 240 insertions(+), 9 deletions(-) diff --git a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertiesConfigSource.java b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertiesConfigSource.java index a2749d8..142d8b7 100644 --- a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertiesConfigSource.java +++ b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertiesConfigSource.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.Properties; /** - * Base class for configuration sources based on {@link Properties} object. + * Base class for configuration sources based on a fixed {@link Properties} object. */ public abstract class PropertiesConfigSource extends BaseConfigSource { @@ -50,7 +50,7 @@ public abstract class PropertiesConfigSource extends BaseConfigSource @Override public Map<String, String> getProperties() { - Map<String,String> result = new HashMap<String, String>(); + Map<String,String> result = new HashMap<String, String>(properties.size()); for (String propertyName : properties.stringPropertyNames()) { result.put(propertyName, properties.getProperty(propertyName)); diff --git a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertyFileConfigSource.java b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertyFileConfigSource.java index 5f15c6f..c600176 100644 --- a/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertyFileConfigSource.java +++ b/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/PropertyFileConfigSource.java @@ -19,30 +19,227 @@ package org.apache.deltaspike.core.impl.config; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; +import org.apache.deltaspike.core.api.config.ConfigResolver; import org.apache.deltaspike.core.util.PropertyFileUtils; /** * {@link org.apache.deltaspike.core.spi.config.ConfigSource} which uses - * <i>META-INF/apache-deltaspike.properties</i> for the lookup + * a fixed property file for the lookup. + * + * If the property file has a 'file://' protocol, we are able to pick up + * changes during runtime when the underlying property file changes. + * This does not make sense for property files in JARs, but makes perfect sense + * whenever a property file URL is directly on the file system. */ -class PropertyFileConfigSource extends PropertiesConfigSource +public class PropertyFileConfigSource extends BaseConfigSource { - private String fileName; + /** + * The name of a property which can be defined inside the property file + * to define the amount of seconds after which the property file should + * be tested for changes again. + * Note that the test is performed by storing the lastChanged attribute of the + * underlying file. + * + * By default the time after which we look for changes is {@link #RELOAD_PERIOD_DEFAULT}. + * This can be changed by explicitly adding a property with the name defined in {@link #RELOAD_PERIOD} + * which contains the number of seconds after which we try to reload again. + * A zero or negative value means no dynamic reloading. + * <pre> + * # look for changes after 60 seconds + * deltaspike_reload=60 + * </pre> + * Whether the file got changed is determined by the lastModifiedDate of the underlying file. + */ + public static final String RELOAD_PERIOD = "deltaspike_reload"; + public static final int RELOAD_PERIOD_DEFAULT = 300; + + private final ConfigResolver.ConfigHelper configHelper; + + /** + * currently loaded config properties. + */ + private Map<String, String> properties; + + private final URL propertyFileUrl; + private String filePath; - PropertyFileConfigSource(URL propertyFileUrl) + private int reloadAllSeconds = RELOAD_PERIOD_DEFAULT; + private Instant fileLastModified = null; + + /** + * Reload after that time in seconds. + */ + private int reloadAfterSec; + + private Consumer<Set<String>> reportAttributeChange; + + public PropertyFileConfigSource(URL propertyFileUrl) { - super(PropertyFileUtils.loadProperties(propertyFileUrl)); - fileName = propertyFileUrl.toExternalForm(); + this.propertyFileUrl = propertyFileUrl; + filePath = propertyFileUrl.toExternalForm(); + + this.properties = toMap(PropertyFileUtils.loadProperties(propertyFileUrl)); + + if (isFile(propertyFileUrl)) + { + fileLastModified = getLastModified(); + configHelper = ConfigResolver.getConfigProvider().getHelper(); + + calculateReloadTime(); + reloadAfterSec = getNowSeconds() + reloadAllSeconds; + } + else + { + configHelper = null; + } + initOrdinal(100); } + private void calculateReloadTime() + { + final String reloadPeriod = properties.get(RELOAD_PERIOD); + if (reloadPeriod != null) + { + try + { + reloadAllSeconds = Integer.parseInt(reloadPeriod); + } + catch (NumberFormatException nfe) + { + log.warning("Wrong value for " + RELOAD_PERIOD + " property: " + reloadPeriod + + ". Must be numeric in seconds. Using default " + RELOAD_PERIOD_DEFAULT); + reloadAllSeconds = RELOAD_PERIOD_DEFAULT; + } + } + } + + protected Map<String, String> toMap(Properties properties) + { + Map<String,String> result = new HashMap<>(properties.size()); + for (String propertyName : properties.stringPropertyNames()) + { + result.put(propertyName, properties.getProperty(propertyName)); + } + + return Collections.unmodifiableMap(result); + } + + @Override + public Map<String, String> getProperties() + { + if (needsReload()) + { + reloadProperties(); + } + + return properties; + } + + @Override + public String getPropertyValue(String key) + { + if (needsReload()) + { + reloadProperties(); + } + + return properties.get(key); + } + + private boolean needsReload() + { + if (fileLastModified != null && getNowSeconds() > reloadAfterSec) + { + final Instant newLastModified = getLastModified(); + if (newLastModified != null && newLastModified.isAfter(fileLastModified)) + { + return true; + } + } + + return false; + } + + private synchronized void reloadProperties() + { + // another thread might have already updated the properties. + if (needsReload()) + { + final Map<String, String> newProps = toMap(PropertyFileUtils.loadProperties(propertyFileUrl)); + + final Set<String> modfiedAttributes = configHelper.diffConfig(properties, newProps); + if (!modfiedAttributes.isEmpty()) + { + reportAttributeChange.accept(modfiedAttributes); + } + + this.properties = newProps; + + fileLastModified = getLastModified(); + + calculateReloadTime(); + reloadAfterSec = getNowSeconds() + reloadAllSeconds; + } + } + + private int getNowSeconds() + { + // this might overrun all 100 years or so. + // I think we can live with a faster reload all 100 years + // if we can spare needing to deal with atomic updates ;) + return (int) TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()); + } + + private Instant getLastModified() + { + try + { + return Files.getLastModifiedTime(Paths.get(propertyFileUrl.toURI())).toInstant(); + } + catch (Exception e) + { + log.log(Level.WARNING, + "Cannot dynamically reload property file {0}. Not able to read last modified date", filePath); + return null; + } + } + + private boolean isFile(URL propertyFileUrl) + { + return "file".equalsIgnoreCase(propertyFileUrl.getProtocol()); + } + /** * {@inheritDoc} */ @Override public String getConfigName() { - return fileName; + return filePath; + } + + @Override + public void setOnAttributeChange(Consumer<Set<String>> reportAttributeChange) + { + this.reportAttributeChange = reportAttributeChange; + } + + @Override + public boolean isScannable() + { + return true; } } diff --git a/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/propertyconfigsource/BaseTestConfigProperty.java b/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/propertyconfigsource/BaseTestConfigProperty.java index 61ababe..ffec0ce 100644 --- a/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/propertyconfigsource/BaseTestConfigProperty.java +++ b/deltaspike/core/impl/src/test/java/org/apache/deltaspike/test/core/api/config/propertyconfigsource/BaseTestConfigProperty.java @@ -20,12 +20,20 @@ package org.apache.deltaspike.test.core.api.config.propertyconfigsource; import javax.inject.Inject; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.util.Collections; + +import org.apache.deltaspike.core.api.config.ConfigResolver; +import org.apache.deltaspike.core.impl.config.PropertyFileConfigSource; import org.junit.Assert; import org.junit.Test; public class BaseTestConfigProperty { protected final static String CONFIG_FILE_NAME = "myconfig.properties"; + protected static final String CONFIG_VALUE = "deltaspike.dynamic.reloadable.config.value"; @Inject private MyBean myBean; @@ -44,6 +52,32 @@ public class BaseTestConfigProperty Assert.assertEquals(8589934592l, myBean.getLongConfig()); Assert.assertEquals(-1.1f, myBean.getFloatConfig(), 0); Assert.assertEquals(4e40, myBean.getDoubleConfig(), 0); + } + + @Test + public void testDynamicReload() throws Exception + { + File prop = File.createTempFile("deltaspike-test", ".properties"); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(prop))) + { + bw.write(CONFIG_VALUE + "=1\ndeltaspike_reload=1\n"); + bw.flush(); + } + prop.deleteOnExit(); + + final PropertyFileConfigSource dynamicReloadConfigSource = new PropertyFileConfigSource(prop.toURI().toURL()); + ConfigResolver.addConfigSources(Collections.singletonList(dynamicReloadConfigSource)); + + Assert.assertEquals("1", ConfigResolver.getPropertyValue(CONFIG_VALUE)); + + Thread.sleep(1600L); + + try (BufferedWriter bw = new BufferedWriter(new FileWriter(prop))) + { + bw.write(CONFIG_VALUE + "=2\ndeltaspike_reload=1\n"); + bw.flush(); + } + Assert.assertEquals("2", ConfigResolver.getPropertyValue(CONFIG_VALUE)); } }
