Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ switcher.check -> true/false When true, it will check Switcher Keys
switcher.relay.restrict -> true/false When true, it will check snapshot relay status
switcher.snapshot.location -> Folder from where snapshots will be saved/read
switcher.snapshot.auto -> true/false Automated lookup for snapshot when initializing the client
switcher.snapshot.watcher -> true/false Enable the watcher to monitor the snapshot file for changes during runtime
switcher.snapshot.skipvalidation -> true/false Skip snapshotValidation() that can be used for UT executions
switcher.snapshot.updateinterval -> Enable the Snapshot Auto Update given an interval of time - e.g. 1s (s: seconds, m: minutes)
switcher.silent -> Enable contigency given the time for the client to retry - e.g. 5s (s: seconds - m: minutes - h: hours)
Expand Down Expand Up @@ -252,9 +253,9 @@ MyAppFeatures.scheduleSnapshotAutoUpdate("5s", new SnapshotCallback() {
});
```

## Real-time snapshot reload
## Real-time snapshot reload (Hot-swapping)
Let the Switcher Client manage your application local snapshot.<br>
These features allow you to configure the SDK to automatically update the snapshot in the background.
These features allow you to configure the SDK to automatically update the snapshot during runtime.

1. This feature will update the in-memory Snapshot every time the file is modified.

Expand All @@ -263,6 +264,15 @@ MyAppFeatures.watchSnapshot();
MyAppFeatures.stopWatchingSnapshot();
```

Alternatively, you can also set the Switcher Context configuration to start watching the snapshot file during the client initialization.

```java
MyAppFeatures.configure(ContextBuilder.builder()
.snapshotWatcher(true));

MyAppFeatures.initializeClient();
```

2. You can also perform snapshot update validation to verify if there are changes to be pulled.

```java
Expand Down Expand Up @@ -319,7 +329,6 @@ Alternatively, you can also set the Switcher Context configuration to check duri

```java
MyAppFeatures.configure(ContextBuilder.builder()
...
.checkSwitchers(true));

MyAppFeatures.initializeClient();
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@

<!-- test -->
<okhttp.version>5.0.0-alpha.16</okhttp.version>
<junit-jupiter.version>5.13.0</junit-jupiter.version>
<junit-jupiter.version>5.13.1</junit-jupiter.version>
<junit-pioneer.version>2.3.0</junit-pioneer.version>
<junit-platform-launcher.version>1.13.0</junit-platform-launcher.version>
<junit-platform-launcher.version>1.13.1</junit-platform-launcher.version>

<!-- Plugins -->
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ public ContextBuilder snapshotSkipValidation(boolean snapshotSkipValidation) {
return this;
}

/**
* @param snapshotWatcher true/false When true, it will watch the snapshot file for changes and update the switchers accordingly
* @return ContextBuilder
*/
public ContextBuilder snapshotWatcher(boolean snapshotWatcher) {
switcherProperties.setValue(ContextKey.SNAPSHOT_WATCHER, snapshotWatcher);
return this;
}

/**
* @param retryAfter Enable contingency given the time for the client to retry - e.g. 5s (s: seconds - m: minutes - h: hours)
* @return ContextBuilder
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/github/switcherapi/client/SwitcherConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ protected void updateSwitcherConfig(SwitcherProperties properties) {
snapshotConfig.setLocation(properties.getValue(ContextKey.SNAPSHOT_LOCATION));
snapshotConfig.setAuto(properties.getBoolean(ContextKey.SNAPSHOT_AUTO_LOAD));
snapshotConfig.setSkipValidation(properties.getBoolean(ContextKey.SNAPSHOT_SKIP_VALIDATION));
snapshotConfig.setWatcher(properties.getBoolean(ContextKey.SNAPSHOT_WATCHER));
snapshotConfig.setUpdateInterval(properties.getValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL));
setSnapshot(snapshotConfig);

Expand Down Expand Up @@ -143,6 +144,7 @@ public static class SnapshotConfig {
private String location;
private boolean auto;
private boolean skipValidation;
private boolean watcher;
private String updateInterval;

public String getLocation() {
Expand All @@ -169,6 +171,14 @@ public void setSkipValidation(boolean skipValidation) {
this.skipValidation = skipValidation;
}

public boolean isWatcher() {
return watcher;
}

public void setWatcher(boolean watcher) {
this.watcher = watcher;
}

public String getUpdateInterval() {
return updateInterval;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ protected void configureClient() {
.poolConnectionSize(poolSize)
.snapshotLocation(snapshot.getLocation())
.snapshotAutoLoad(snapshot.isAuto())
.snapshotWatcher(snapshot.isWatcher())
.snapshotSkipValidation(snapshot.isSkipValidation())
.snapshotAutoUpdateInterval(snapshot.getUpdateInterval())
.truststorePath(truststore.getPath())
Expand Down Expand Up @@ -184,6 +185,7 @@ public static void initializeClient() {
switcherExecutor = buildInstance();

loadSwitchers();
scheduleSnapshotWatcher();
scheduleSnapshotAutoUpdate(contextStr(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL));
ContextBuilder.preConfigure(switcherProperties);
SwitcherUtils.debug(logger, "Switcher Client initialized");
Expand Down Expand Up @@ -269,6 +271,18 @@ private static void loadSwitchers() throws SwitchersValidationException {
}
}

/**
* Schedule a task to watch the snapshot file for modifications.<br>
* The task will be executed in a single thread executor service.
* <p>
* (*) Requires client to use local settings
*/
private static void scheduleSnapshotWatcher() {
if (contextBol(ContextKey.SNAPSHOT_WATCHER)) {
watchSnapshot();
}
}

/**
* Schedule a task to update the snapshot automatically.<br>
* The task will be executed in a single thread executor service.
Expand Down Expand Up @@ -387,8 +401,8 @@ public static SwitcherRequest getSwitcher(String key) {
}

/**
* Validate if the snapshot version is the same as the one in the API.<br>
* If the version is different, it will update the snapshot in memory.
* Validate if the local snapshot version is the same as remote.<br>
* If the version is different, it will update the local snapshot.
*
* @return true if snapshot was updated
*/
Expand All @@ -403,7 +417,7 @@ public static boolean validateSnapshot() {

/**
* Start watching snapshot files for modifications.<br>
* When the file is modified the in-memory snapshot will reload
* When the file is modified the local snapshot will reload
*
* <p>
* (*) Requires client to use local settings
Expand All @@ -414,7 +428,7 @@ public static void watchSnapshot() {

/**
* Start watching snapshot files for modifications.<br>
* When the file is modified the in-memory snapshot will reload
* When the file is modified the local snapshot will reload
*
* <p>
* (*) Requires client to use local settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,58 @@

import com.github.switcherapi.client.exception.SwitcherContextException;
import com.github.switcherapi.client.model.ContextKey;
import com.github.switcherapi.client.utils.SwitcherUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import static com.github.switcherapi.client.remote.Constants.*;
import static com.github.switcherapi.client.utils.SwitcherUtils.*;

public class SwitcherPropertiesImpl implements SwitcherProperties {

private final Map<String, Object> properties = new HashMap<>();

public SwitcherPropertiesImpl() {
initDefaults();
}

private void initDefaults() {
setValue(ContextKey.ENVIRONMENT, DEFAULT_ENV);
setValue(ContextKey.REGEX_TIMEOUT, DEFAULT_REGEX_TIMEOUT);
setValue(ContextKey.TIMEOUT_MS, DEFAULT_TIMEOUT);
setValue(ContextKey.POOL_CONNECTION_SIZE, DEFAULT_POOL_SIZE);
setValue(ContextKey.SNAPSHOT_AUTO_LOAD, false);
setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, false);
setValue(ContextKey.SNAPSHOT_WATCHER, false);
setValue(ContextKey.LOCAL_MODE, false);
setValue(ContextKey.CHECK_SWITCHERS, false);
setValue(ContextKey.RESTRICT_RELAY, true);
}

@Override
public void loadFromProperties(Properties prop) {
setValue(ContextKey.CONTEXT_LOCATION, SwitcherUtils.resolveProperties(ContextKey.CONTEXT_LOCATION.getParam(), prop));
setValue(ContextKey.URL, SwitcherUtils.resolveProperties(ContextKey.URL.getParam(), prop));
setValue(ContextKey.APIKEY, SwitcherUtils.resolveProperties(ContextKey.APIKEY.getParam(), prop));
setValue(ContextKey.DOMAIN, SwitcherUtils.resolveProperties(ContextKey.DOMAIN.getParam(), prop));
setValue(ContextKey.COMPONENT, SwitcherUtils.resolveProperties(ContextKey.COMPONENT.getParam(), prop));
setValue(ContextKey.ENVIRONMENT, getValueDefault(SwitcherUtils.resolveProperties(ContextKey.ENVIRONMENT.getParam(), prop), DEFAULT_ENV));
setValue(ContextKey.SNAPSHOT_LOCATION, SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_LOCATION.getParam(), prop));
setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_SKIP_VALIDATION.getParam(), prop), false));
setValue(ContextKey.SNAPSHOT_AUTO_LOAD, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_AUTO_LOAD.getParam(), prop), false));
setValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL, SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL.getParam(), prop));
setValue(ContextKey.SILENT_MODE, SwitcherUtils.resolveProperties(ContextKey.SILENT_MODE.getParam(), prop));
setValue(ContextKey.LOCAL_MODE, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
setValue(ContextKey.TRUSTSTORE_PATH, SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
setValue(ContextKey.TRUSTSTORE_PASSWORD, SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));
setValue(ContextKey.TIMEOUT_MS, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.TIMEOUT_MS.getParam(), prop), DEFAULT_TIMEOUT));
setValue(ContextKey.POOL_CONNECTION_SIZE, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.POOL_CONNECTION_SIZE.getParam(), prop), DEFAULT_POOL_SIZE));
setValue(ContextKey.CONTEXT_LOCATION, resolveProperties(ContextKey.CONTEXT_LOCATION.getParam(), prop));
setValue(ContextKey.URL, resolveProperties(ContextKey.URL.getParam(), prop));
setValue(ContextKey.APIKEY, resolveProperties(ContextKey.APIKEY.getParam(), prop));
setValue(ContextKey.DOMAIN, resolveProperties(ContextKey.DOMAIN.getParam(), prop));
setValue(ContextKey.COMPONENT, resolveProperties(ContextKey.COMPONENT.getParam(), prop));
setValue(ContextKey.ENVIRONMENT, getValueDefault(resolveProperties(ContextKey.ENVIRONMENT.getParam(), prop), DEFAULT_ENV));
setValue(ContextKey.SNAPSHOT_LOCATION, resolveProperties(ContextKey.SNAPSHOT_LOCATION.getParam(), prop));
setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, getBoolDefault(resolveProperties(ContextKey.SNAPSHOT_SKIP_VALIDATION.getParam(), prop), false));
setValue(ContextKey.SNAPSHOT_AUTO_LOAD, getBoolDefault(resolveProperties(ContextKey.SNAPSHOT_AUTO_LOAD.getParam(), prop), false));
setValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL, resolveProperties(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL.getParam(), prop));
setValue(ContextKey.SNAPSHOT_WATCHER, getBoolDefault(resolveProperties(ContextKey.SNAPSHOT_WATCHER.getParam(), prop), false));
setValue(ContextKey.SILENT_MODE, resolveProperties(ContextKey.SILENT_MODE.getParam(), prop));
setValue(ContextKey.LOCAL_MODE, getBoolDefault(resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
setValue(ContextKey.TRUSTSTORE_PATH, resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
setValue(ContextKey.TRUSTSTORE_PASSWORD, resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));
setValue(ContextKey.TIMEOUT_MS, getIntDefault(resolveProperties(ContextKey.TIMEOUT_MS.getParam(), prop), DEFAULT_TIMEOUT));
setValue(ContextKey.POOL_CONNECTION_SIZE, getIntDefault(resolveProperties(ContextKey.POOL_CONNECTION_SIZE.getParam(), prop), DEFAULT_POOL_SIZE));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public enum ContextKey {
* (String) Interval given to the library to update the snapshot
*/
SNAPSHOT_AUTO_UPDATE_INTERVAL("switcher.snapshot.updateinterval"),

/**
* (boolean) Defines if the client will watch the snapshot file for changes and update the switchers accordingly. (default is false)
*/
SNAPSHOT_WATCHER("switcher.snapshot.watcher"),

/**
* (String) Defines if client will work in silent mode by specifying the time interval to retry
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.github.switcherapi.client.utils;

import com.github.switcherapi.SwitchersBase;
import com.github.switcherapi.client.model.criteria.Data;
import com.github.switcherapi.client.model.criteria.Domain;
import com.github.switcherapi.client.model.criteria.Snapshot;
import com.github.switcherapi.client.service.WorkerName;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;

import static org.junit.jupiter.api.Assertions.assertEquals;

abstract class SnapshotTest {

private static final Logger logger = LoggerFactory.getLogger(SnapshotTest.class);

protected static final String SNAPSHOTS_LOCAL = Paths.get(StringUtils.EMPTY).toAbsolutePath() + "/src/test/resources";

protected static void removeGeneratedFiles() throws IOException {
SwitchersBase.stopWatchingSnapshot();
Files.deleteIfExists(Paths.get(SNAPSHOTS_LOCAL + "\\generated_watcher_default.json"));
}

protected static void generateFixture() {
final Snapshot mockedSnapshot = new Snapshot();
final Data data = new Data();
data.setDomain(SnapshotLoader.loadSnapshot(SNAPSHOTS_LOCAL + "/snapshot_watcher.json"));
mockedSnapshot.setData(data);

SnapshotLoader.saveSnapshot(mockedSnapshot, SNAPSHOTS_LOCAL, "generated_watcher_default");
}

protected void changeFixture() {
final Snapshot mockedSnapshot = new Snapshot();
final Data data = new Data();
data.setDomain(SnapshotLoader.loadSnapshot(SNAPSHOTS_LOCAL + "/snapshot_watcher.json"));
mockedSnapshot.setData(data);

data.setDomain(new Domain(
data.getDomain().getName(),
data.getDomain().getDescription(),
!data.getDomain().isActivated(),
data.getDomain().getVersion(),
data.getDomain().getGroup()));

final Gson gson = new GsonBuilder().setPrettyPrinting().create();
writeFixture(gson.toJson(mockedSnapshot));
}

protected void writeFixture(String content) {
try (
final FileWriter fileWriter = new FileWriter(
String.format("%s/%s.json", SNAPSHOTS_LOCAL, "generated_watcher_default"));

final BufferedWriter bw = new BufferedWriter(fileWriter);
final PrintWriter wr = new PrintWriter(bw)) {
wr.write(content);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}

protected void assertWorker(boolean exists) {
assertEquals(exists, Thread.getAllStackTraces().keySet().stream()
.anyMatch(t -> t.getName().equals(WorkerName.SNAPSHOT_WATCH_WORKER.toString())));
}
}
Loading