Skip to content

Commit

Permalink
Feat/proxy reload (#133)
Browse files Browse the repository at this point in the history
* feat: Proxy cache inherits from passthrough. If the key isnt in the cache fallback to the passthough and cache the result.

* feat: register the proxy value with gestalt as a CoreReloadListener to get notified when Gestalt reloads its configs.
When Gestalt reloads its configs clear the cache, it will refill as its methods get called.
  • Loading branch information
credmond-git authored Dec 21, 2023
1 parent 6fddaa8 commit 910afae
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.github.gestalt.config.node.LeafNode;
import org.github.gestalt.config.node.MapNode;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.reload.CoreReloadListener;
import org.github.gestalt.config.tag.Tags;
import org.github.gestalt.config.utils.PathUtil;
import org.github.gestalt.config.utils.ValidateOf;
Expand All @@ -28,10 +29,12 @@
* @author <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public final class ProxyDecoder implements Decoder<Object> {

// For the proxy decoder, if we should use a cached value or call gestalt for the most recent value.
private ProxyDecoderMode proxyDecoderMode = ProxyDecoderMode.CACHE;

private static String getConfigName(String methodName, Type returnType) {

private static String getConfigNameFromMethod(String methodName, Type returnType) {
String name = methodName;
if (methodName.startsWith("get")) {
name = methodName.substring(3);
Expand Down Expand Up @@ -104,7 +107,7 @@ public ValidateOf<Object> decode(String path, Tags tags, ConfigNode node, TypeCa
if (configAnnotation != null && configAnnotation.path() != null && !configAnnotation.path().isEmpty()) {
name = configAnnotation.path();
} else {
name = getConfigName(methodName, returnType);
name = getConfigNameFromMethod(methodName, returnType);
}

String nextPath = PathUtil.pathForKey(path, name);
Expand Down Expand Up @@ -152,7 +155,10 @@ public ValidateOf<Object> decode(String path, Tags tags, ConfigNode node, TypeCa

case CACHE:
default: {
proxyHandler = new ProxyCacheInvocationHandler(path, methodResults);
proxyHandler = new ProxyCacheInvocationHandler(path, tags, decoderContext, methodResults);
if(decoderContext.getGestalt() != null) {
decoderContext.getGestalt().registerListener((ProxyCacheInvocationHandler) proxyHandler);
}
break;
}
}
Expand All @@ -161,46 +167,11 @@ public ValidateOf<Object> decode(String path, Tags tags, ConfigNode node, TypeCa
return ValidateOf.validateOf(myProxy, errors);
}

private static class ProxyCacheInvocationHandler implements InvocationHandler {
private final String path;
private final Map<String, Object> methodResults;


private ProxyCacheInvocationHandler(String path, Map<String, Object> methodResults) {
this.path = path;
this.methodResults = methodResults;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
boolean isDefault = method.isDefault();

Object result = methodResults.get(methodName);

Class<?> type = method.getReturnType();
if (result != null) {
return result;
} else if (isDefault) {
return MethodHandles.lookup()
.findSpecial(
method.getDeclaringClass(),
methodName,
MethodType.methodType(type, new Class[0]),
method.getDeclaringClass())
.bindTo(proxy)
.invokeWithArguments(args);
} else {
throw new GestaltException("Failed to get cached object from proxy config while calling method: " + methodName +
" with type: " + type + " in path: " + path + ".");
}
}
}

private static class ProxyPassThroughInvocationHandler implements InvocationHandler {
private final String path;
private final Tags tags;
private final DecoderContext decoderContext;
static class ProxyPassThroughInvocationHandler implements InvocationHandler {
protected final String path;
protected final Tags tags;
protected final DecoderContext decoderContext;


private ProxyPassThroughInvocationHandler(String path, Tags tags, DecoderContext decoderContext) {
Expand All @@ -211,6 +182,16 @@ private ProxyPassThroughInvocationHandler(String path, Tags tags, DecoderContext

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?> returnType = method.getReturnType();

Optional<Object> result = retrieveConfig(proxy, method, args);
return result.orElseThrow(() ->
new GestaltException("Failed to get pass through object from proxy config while calling method: " + methodName +
" with type: " + returnType + " in path: " + path + "."));
}

protected Optional<Object> retrieveConfig(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
boolean isDefault = method.isDefault();

Expand All @@ -224,15 +205,18 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
if (configAnnotation != null && configAnnotation.path() != null && !configAnnotation.path().isEmpty()) {
name = configAnnotation.path();
} else {
name = getConfigName(methodName, returnType);
name = getConfigNameFromMethod(methodName, returnType);
}

String nextPath = PathUtil.pathForKey(path, name);

var result = decoderContext.getGestalt().getConfigOptional(nextPath, TypeCapture.of(genericType), tags);
Optional<Object> result = Optional.empty();
if(decoderContext.getGestalt() != null) {
result = decoderContext.getGestalt().getConfigOptional(nextPath, TypeCapture.of(genericType), tags);
}

if (result != null && result.isPresent()) {
return result.get();
return result;
} else {

// if we have no value, check the config annotation for a default.
Expand All @@ -243,23 +227,64 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
decoderContext);

if (defaultValidateOf.hasResults()) {
return defaultValidateOf.results();
return Optional.of(defaultValidateOf.results());
}
}

if (isDefault) {
return MethodHandles.lookup()
var defaultResult = MethodHandles.lookup()
.findSpecial(
method.getDeclaringClass(),
methodName,
MethodType.methodType(returnType, new Class[0]),
method.getDeclaringClass())
.bindTo(proxy)
.invokeWithArguments(args);

return Optional.of(defaultResult);
}
}
throw new GestaltException("Failed to get pass through object from proxy config while calling method: " + methodName +
" with type: " + returnType + " in path: " + path + ".");
return Optional.empty();
}
}


static class ProxyCacheInvocationHandler extends ProxyPassThroughInvocationHandler
implements InvocationHandler, CoreReloadListener {

private static final System.Logger logger = System.getLogger(ProxyCacheInvocationHandler.class.getName());
private final Map<String, Object> methodResults;


private ProxyCacheInvocationHandler(String path, Tags tags, DecoderContext decoderContext, Map<String, Object> methodResults) {
super(path, tags, decoderContext);
this.methodResults = methodResults;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?> returnType = method.getReturnType();

Object result = methodResults.get(methodName);
if (result != null) {
return result;
} else {
Optional<Object> resultOptional = retrieveConfig(proxy, method, args);
var gestaltResult = resultOptional.orElseThrow(() ->
new GestaltException("Failed to get cached object from proxy config while calling method: " + methodName +
" with type: " + returnType + " in path: " + path + "."));

methodResults.put(methodName, gestaltResult);

return gestaltResult;
}
}

@Override
public void reload() {
logger.log(System.Logger.Level.DEBUG, "Reloading received on Proxy Cache Listener. Clearing Cache");
methodResults.clear();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.github.gestalt.config.reload;

import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

/**
* Store all core reload listeners and functionality to call the on reload.
Expand All @@ -12,7 +11,7 @@ public class CoreReloadListenersContainer {
/**
* Listeners for the core reload.
*/
protected final List<CoreReloadListener> listeners = new ArrayList<>();
protected final WeakHashMap<Integer, CoreReloadListener> listeners = new WeakHashMap<>();

/**
* Default constructor for CoreReloadStrategy.
Expand All @@ -26,7 +25,7 @@ public CoreReloadListenersContainer() {
* @param listener to register
*/
public void registerListener(CoreReloadListener listener) {
listeners.add(listener);
listeners.put(listener.hashCode(), listener);
}

/**
Expand All @@ -35,13 +34,13 @@ public void registerListener(CoreReloadListener listener) {
* @param listener to remove
*/
public void removeListener(CoreReloadListener listener) {
listeners.remove(listener);
listeners.remove(listener.hashCode());
}

/**
* called when the core has reloaded.
*/
public void reload() {
listeners.forEach(CoreReloadListener::reload);
listeners.forEach((k, v) -> v.reload());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.github.gestalt.config.decoder;

import org.github.gestalt.config.Gestalt;
import org.github.gestalt.config.builder.GestaltBuilder;
import org.github.gestalt.config.entity.ValidationLevel;
import org.github.gestalt.config.exceptions.GestaltConfigurationException;
import org.github.gestalt.config.exceptions.GestaltException;
Expand All @@ -8,6 +10,8 @@
import org.github.gestalt.config.node.*;
import org.github.gestalt.config.path.mapper.StandardPathMapper;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.reload.ManualConfigReloadStrategy;
import org.github.gestalt.config.source.MapConfigSourceBuilder;
import org.github.gestalt.config.tag.Tags;
import org.github.gestalt.config.test.classes.*;
import org.github.gestalt.config.utils.ValidateOf;
Expand Down Expand Up @@ -520,5 +524,127 @@ void decodeAnnotationsOnlyDefault() {
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("mysql.com", results.getUri());
}

@Test
void decodeReload() throws GestaltException {

// Create a map of configurations we wish to inject.
Map<String, String> configs = new HashMap<>();
configs.put("db.port", "100");
configs.put("db.uri", "mysql.com");
configs.put("db.password", "pass");

ManualConfigReloadStrategy reload = new ManualConfigReloadStrategy();

// using the builder to layer on the configuration files.
// The later ones layer on and over write any values in the previous
GestaltBuilder builder = new GestaltBuilder();
Gestalt gestalt = builder
.addSource(MapConfigSourceBuilder.builder()
.setCustomConfig(configs)
.addConfigReloadStrategy(reload)
.build())
.setTreatNullValuesInClassAsErrors(false)
.setProxyDecoderMode(ProxyDecoderMode.CACHE)
.build();

gestalt.loadConfigs();


DBInfoInterface results = gestalt.getConfig("db", DBInfoInterface.class);

Assertions.assertEquals(100, results.getPort());
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("mysql.com", results.getUri());

configs.put("db.port", "200");
reload.reload();

Assertions.assertEquals(200, results.getPort());
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("mysql.com", results.getUri());
}

@Test
void decodeReloadDefault() throws GestaltException {

// Create a map of configurations we wish to inject.
Map<String, String> configs = new HashMap<>();
configs.put("db.port", "100");
configs.put("db.uri", "mysql.com");
configs.put("db.password", "pass");

ManualConfigReloadStrategy reload = new ManualConfigReloadStrategy();

// using the builder to layer on the configuration files.
// The later ones layer on and over write any values in the previous
GestaltBuilder builder = new GestaltBuilder();
Gestalt gestalt = builder
.addSource(MapConfigSourceBuilder.builder()
.setCustomConfig(configs)
.addConfigReloadStrategy(reload)
.build())
.setTreatNullValuesInClassAsErrors(false)
.setProxyDecoderMode(ProxyDecoderMode.CACHE)
.build();

gestalt.loadConfigs();


DBInfoInterface results = gestalt.getConfig("db", DBInfoInterface.class);

Assertions.assertEquals(100, results.getPort());
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("mysql.com", results.getUri());

configs.remove("db.port");
configs.put("db.uri", "postgresql.org");
reload.reload();

Assertions.assertEquals(10, results.getPort());
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("postgresql.org", results.getUri());
}

@Test
void decodeReloadDAnnotationDefault() throws GestaltException {

// Create a map of configurations we wish to inject.
Map<String, String> configs = new HashMap<>();
configs.put("db.channel", "100");
configs.put("db.uri", "mysql.com");
configs.put("db.password", "pass");

ManualConfigReloadStrategy reload = new ManualConfigReloadStrategy();

// using the builder to layer on the configuration files.
// The later ones layer on and over write any values in the previous
GestaltBuilder builder = new GestaltBuilder();
Gestalt gestalt = builder
.addSource(MapConfigSourceBuilder.builder()
.setCustomConfig(configs)
.addConfigReloadStrategy(reload)
.build())
.setTreatNullValuesInClassAsErrors(false)
.setProxyDecoderMode(ProxyDecoderMode.CACHE)
.build();

gestalt.loadConfigs();


IDBInfoAnnotations results = gestalt.getConfig("db", IDBInfoAnnotations.class);

Assertions.assertEquals(100, results.getPort());
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("mysql.com", results.getUri());

configs.remove("db.channel");
configs.put("db.uri", "postgresql.org");
reload.reload();

Assertions.assertEquals(1234, results.getPort());
Assertions.assertEquals("pass", results.getPassword());
Assertions.assertEquals("postgresql.org", results.getUri());
}
}

0 comments on commit 910afae

Please sign in to comment.