Skip to content

Commit

Permalink
Add Configuration.refresh() for manual refreshing of ConfigurationSou…
Browse files Browse the repository at this point in the history
…rce plugins
  • Loading branch information
rbygrave committed Feb 3, 2024
1 parent 3d65c4d commit 016183c
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import io.avaje.config.ConfigurationSource;

import java.io.StringReader;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

import static java.lang.System.Logger.Level.*;

Expand All @@ -18,30 +21,40 @@ public final class AwsAppConfigPlugin implements ConfigurationSource {

private static final System.Logger log = System.getLogger("io.avaje.config.AwsAppConfig");

private Loader loader;

@Override
public void load(Configuration configuration) {
if (!configuration.enabled("aws.appconfig.enabled", true)) {
log.log(INFO, "AwsAppConfig plugin is disabled");
return;
}
var loader = new Loader(configuration);
loader.schedule();
loader = new Loader(configuration);
loader.reload();
}

@Override
public void refresh() {
if (loader != null) {
loader.reload();
}
}

static final class Loader {

private final Configuration configuration;
private final AppConfigFetcher fetcher;
private final ConfigParser yamlParser;
private final ConfigParser propertiesParser;
private final long frequency;
private final ReentrantLock lock = new ReentrantLock();
private final AtomicReference<Instant> validUntil;
private final long nextRefreshSeconds;

private String currentVersion = "none";

Loader(Configuration configuration) {
this.validUntil = new AtomicReference<>(Instant.now().minusSeconds(1));
this.configuration = configuration;
this.frequency = configuration.getLong("aws.appconfig.frequency", 60L);
this.propertiesParser = configuration.parser("properties").orElseThrow();
this.yamlParser = configuration.parser("yaml").orElse(null);
if (yamlParser == null) {
Expand All @@ -57,14 +70,31 @@ static final class Loader {
.environment(env)
.configuration(con)
.build();
}

void schedule() {
configuration.schedule(frequency * 1000L, frequency * 1000L, this::reload);
boolean pollEnabled = configuration.enabled("aws.appconfig.poll.enabled", true);
long pollSeconds = configuration.getLong("aws.appconfig.poll.seconds", 45L);
this.nextRefreshSeconds = configuration.getLong("aws.appconfig.refresh.seconds", pollSeconds - 1);
if (pollEnabled) {
configuration.schedule(pollSeconds * 1000L, pollSeconds * 1000L, this::reload);
}
}

void reload() {
if (reloadRequired()) {
performReload();
}
}

private boolean reloadRequired() {
return validUntil.get().isAfter(Instant.now());
}

private void performReload() {
lock.lock();
try {
if (!reloadRequired()) {
return;
}
AppConfigFetcher.Result result = fetcher.fetch();
if (currentVersion.equals(result.version())) {
log.log(TRACE, "AwsAppConfig unchanged, version {0}", currentVersion);
Expand All @@ -75,14 +105,18 @@ void reload() {
}
Map<String, String> keyValues = parse(result);
configuration.eventBuilder("AwsAppConfig")
.putAll(keyValues)
.publish();
.putAll(keyValues)
.publish();
currentVersion = result.version();
debugLog(result, keyValues.size());
}
// move the next valid until time
validUntil.set(Instant.now().plusSeconds(nextRefreshSeconds));

} catch (Exception e) {
log.log(ERROR, "Error fetching or processing AwsAppConfig", e);
} finally {
lock.unlock();
}
}

Expand Down
9 changes: 9 additions & 0 deletions avaje-config/src/main/java/io/avaje/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,15 @@ default boolean enabled(String key, boolean enabledDefault) {
*/
void loadIntoSystemProperties();

/**
* Trigger a refresh on all {@link ConfigurationSource}.
* <p>
* Generally configuration sources will schedule a periodic refresh of their
* configuration but there are cases like Lambda where it can be useful to
* trigger a refresh explicitly (e.g. on Lambda invocation).
*/
void refresh();

/**
* Return the number of configuration properties.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,15 @@ public interface ConfigurationSource {
* @param configuration The configuration with initially properties.
*/
void load(Configuration configuration);

/**
* Explicitly refresh the configuration source.
* <p>
* Generally the configuration source will schedule a periodic refresh of its
* configuration but there are cases like Lambda where it can be useful to
* trigger a refresh explicitly and manually (e.g. on Lambda invocation).
*/
default void refresh() {
// do nothing by default
}
}
42 changes: 42 additions & 0 deletions avaje-config/src/main/java/io/avaje/config/CoreComponents.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.avaje.config;

import java.util.Collections;
import java.util.List;

final class CoreComponents {

private final ModificationEventRunner runner;
private final ConfigurationLog log;
private final Parsers parsers;
private final List<ConfigurationSource> sources;

CoreComponents(ModificationEventRunner runner, ConfigurationLog log, Parsers parsers, List<ConfigurationSource> sources) {
this.runner = runner;
this.log = log;
this.parsers = parsers;
this.sources = sources;
}

CoreComponents() {
this.runner = new CoreConfiguration.ForegroundEventRunner();
this.log = new DefaultConfigurationLog();
this.parsers = new Parsers();
this.sources = Collections.emptyList();
}

Parsers parsers() {
return parsers;
}

ConfigurationLog log() {
return log;
}

ModificationEventRunner runner() {
return runner;
}

List<ConfigurationSource> sources() {
return sources;
}
}
22 changes: 16 additions & 6 deletions avaje-config/src/main/java/io/avaje/config/CoreConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ final class CoreConfiguration implements Configuration {
private final CoreListValue listValue;
private final CoreSetValue setValue;
private final ModificationEventRunner eventRunner;
private final List<ConfigurationSource> sources;

private boolean loadedSystemProperties;
private FileWatch watcher;
private Timer timer;
private final String pathPrefix;

CoreConfiguration(Parsers parsers, ModificationEventRunner eventRunner, ConfigurationLog log, CoreEntry.CoreMap entries) {
this.parsers = parsers;
this.eventRunner = eventRunner;
this.log = log;
CoreConfiguration(CoreComponents components, CoreEntry.CoreMap entries) {
this.parsers = components.parsers();
this.eventRunner = components.runner();
this.log = components.log();
this.sources = components.sources();
this.properties = new ModifyAwareProperties(entries);
this.listValue = new CoreListValue(this);
this.setValue = new CoreSetValue(this);
Expand All @@ -56,6 +58,7 @@ final class CoreConfiguration implements Configuration {
this.parsers = parent.parsers;
this.eventRunner = parent.eventRunner;
this.log = parent.log;
this.sources = parent.sources;
this.properties = new ModifyAwareProperties(entries);
this.listValue = new CoreListValue(this);
this.setValue = new CoreSetValue(this);
Expand All @@ -66,7 +69,7 @@ final class CoreConfiguration implements Configuration {
* For testing purposes.
*/
CoreConfiguration(CoreEntry.CoreMap entries) {
this(new Parsers(), new ForegroundEventRunner(), new DefaultConfigurationLog(), entries);
this(new CoreComponents(), entries);
}

/**
Expand Down Expand Up @@ -108,7 +111,7 @@ void initSystemProperties() {
}

private void loadSources(Set<String> names) {
for (final ConfigurationSource source : ServiceLoader.load(ConfigurationSource.class)) {
for (ConfigurationSource source : sources) {
source.load(this);
names.add("ConfigurationSource:" + source.getClass().getCanonicalName());
}
Expand Down Expand Up @@ -174,6 +177,13 @@ public void loadIntoSystemProperties() {
loadedSystemProperties = true;
}

@Override
public void refresh() {
for (ConfigurationSource source : sources) {
source.refresh();
}
}

@Override
public Properties asProperties() {
return properties.asProperties();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package io.avaje.config;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.*;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -76,11 +74,16 @@ public Configuration build() {
final var runner = initRunner();
final var log = initLog();
final var parsers = new Parsers();
final var sources = ServiceLoader.load(ConfigurationSource.class).stream()
.map(ServiceLoader.Provider::get)
.collect(Collectors.toList());

var components = new CoreComponents(runner, log, parsers, sources);
if (includeResourceLoading) {
log.preInitialisation();
initialLoader = new InitialLoader(parsers, log, initResourceLoader());
initialLoader = new InitialLoader(components, initResourceLoader());
}
return new CoreConfiguration(parsers, runner, log, initEntries()).postLoad(initialLoader);
return new CoreConfiguration(components, initEntries()).postLoad(initialLoader);
}

private CoreEntry.CoreMap initEntries() {
Expand Down
6 changes: 3 additions & 3 deletions avaje-config/src/main/java/io/avaje/config/InitialLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ enum Source {
private final Set<String> profileResourceLoaded = new HashSet<>();
private final Parsers parsers;

InitialLoader(Parsers parsers, ConfigurationLog log, ResourceLoader resourceLoader) {
this.parsers = parsers;
this.log = log;
InitialLoader(CoreComponents components, ResourceLoader resourceLoader) {
this.parsers = components.parsers();
this.log = components.log();
this.loadContext = new InitialLoadContext(log, resourceLoader);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.avaje.config;

import io.avaje.config.CoreEntry.CoreMap;
import org.example.MyExternalLoader;
import org.junit.jupiter.api.Test;

import java.io.StringReader;
Expand Down Expand Up @@ -177,6 +178,10 @@ void builder_withResources() {

String userHome = System.getProperty("user.home");
assertThat(conf.get("myHome")).isEqualTo("my/" + userHome + "/home");

MyExternalLoader.reset();
conf.refresh();
assertThat(MyExternalLoader.refreshCalled()).isTrue();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class InitialLoaderTest {

private static InitialLoader newInitialLoader() {
return new InitialLoader(new Parsers(), new DefaultConfigurationLog(), new DefaultResourceLoader());
return new InitialLoader(new CoreComponents(), new DefaultResourceLoader());
}

@Test
Expand Down
14 changes: 14 additions & 0 deletions avaje-config/src/test/java/org/example/MyExternalLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

public class MyExternalLoader implements ConfigurationSource {

static boolean refreshCalled;

@Override
public void load(Configuration configuration) {

Expand All @@ -19,4 +21,16 @@ public void load(Configuration configuration) {
configuration.schedule(500, 500, () -> System.out.println("MyExternalLoader task .."));
}

@Override
public void refresh() {
refreshCalled = true;
}

public static boolean refreshCalled() {
return refreshCalled;
}

public static void reset() {
refreshCalled = false;
}
}

0 comments on commit 016183c

Please sign in to comment.