Skip to content

Commit

Permalink
Merge pull request #117 from avaje/feature/extract-propertiesparser
Browse files Browse the repository at this point in the history
Extract PropertiesParser (so that it is accessible to plugins)
  • Loading branch information
rbygrave authored Feb 3, 2024
2 parents ca4d59c + be9ae35 commit aad9045
Show file tree
Hide file tree
Showing 21 changed files with 393 additions and 136 deletions.
3 changes: 2 additions & 1 deletion avaje-aws-appconfig/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
<relativePath/>
</parent>

<artifactId>avaje-aws-appconfig</artifactId>
<groupId>io.avaje</groupId>
<artifactId>avaje-config-aws-appconfig</artifactId>
<version>0.3-SNAPSHOT</version>

<properties>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.avaje.aws.appconfig;
package io.avaje.config.awsappconfig;

public interface AppConfigFetcher {
interface AppConfigFetcher {

static AppConfigFetcher.Builder builder() {
return new DAppConfigFetcher.Builder();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package io.avaje.config.awsappconfig;

import io.avaje.config.ConfigParser;
import io.avaje.config.Configuration;
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.*;

/**
* Plugin that loads AWS AppConfig as Yaml or Properties.
* <p>
* By default, will periodically reload the configuration if it has changed.
*/
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;
}
loader = new Loader(configuration);
loader.reload();
}

@Override
public void reload() {
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 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.propertiesParser = configuration.parser("properties").orElseThrow();
this.yamlParser = configuration.parser("yaml").orElse(null);
if (yamlParser == null) {
log.log(WARNING, "No Yaml parser registered");
}

var app = configuration.get("aws.appconfig.application");
var env = configuration.get("aws.appconfig.environment");
var con = configuration.get("aws.appconfig.configuration", env + "-" + app);

this.fetcher = AppConfigFetcher.builder()
.application(app)
.environment(env)
.configuration(con)
.build();

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);
} else {
String contentType = result.contentType();
if (log.isLoggable(TRACE)) {
log.log(TRACE, "AwsAppConfig fetched version:{0} contentType:{1} body:{2}", result.version(), contentType, result.body());
}
Map<String, String> keyValues = parse(result);
configuration.eventBuilder("AwsAppConfig")
.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();
}
}

private Map<String, String> parse(AppConfigFetcher.Result result) {
ConfigParser parser = parser(result.contentType());
return parser.load(new StringReader(result.body()));
}

private ConfigParser parser(String contentType) {
if (contentType.endsWith("yaml")) {
return yamlParser;
} else {
return propertiesParser;
}
}

private static void debugLog(AppConfigFetcher.Result result, int size) {
if (log.isLoggable(DEBUG)) {
log.log(DEBUG, "AwsAppConfig loaded version {0} with {1} properties", result.version(), size);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.avaje.aws.appconfig;
package io.avaje.config.awsappconfig;

import java.io.IOException;
import java.net.URI;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.avaje.aws.appconfig;
package io.avaje.config.awsappconfig;

final class DResult implements AppConfigFetcher.Result {

private final String version;
private final String contentType;
private final String body;
public DResult(String version, String contentType, String body) {

DResult(String version, String contentType, String body) {
this.version = version;
this.contentType = contentType;
this.body = body;
Expand Down
8 changes: 4 additions & 4 deletions avaje-aws-appconfig/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import io.avaje.aws.appconfig.AppConfigPlugin;
import io.avaje.config.awsappconfig.AwsAppConfigPlugin;

module io.avaje.aws.appconfig {
module io.avaje.config.awsappconfig {

exports io.avaje.aws.appconfig;
exports io.avaje.config.awsappconfig;

requires io.avaje.config;
requires java.net.http;
provides io.avaje.config.ConfigurationSource with AppConfigPlugin;
provides io.avaje.config.ConfigurationSource with AwsAppConfigPlugin;
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
io.avaje.aws.appconfig.AppConfigPlugin
io.avaje.config.awsappconfig.AwsAppConfigPlugin
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 {@link ConfigurationSource#reload()} on all the sources.
* <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 reloadSources();

/**
* 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 reload 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 reload() {
// 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;
}
}
Loading

0 comments on commit aad9045

Please sign in to comment.