Skip to content

Commit 041b635

Browse files
authored
Added switcher.snapshot.watcher configuration (#347)
1 parent 7d0a187 commit 041b635

File tree

12 files changed

+233
-109
lines changed

12 files changed

+233
-109
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ switcher.check -> true/false When true, it will check Switcher Keys
7474
switcher.relay.restrict -> true/false When true, it will check snapshot relay status
7575
switcher.snapshot.location -> Folder from where snapshots will be saved/read
7676
switcher.snapshot.auto -> true/false Automated lookup for snapshot when initializing the client
77+
switcher.snapshot.watcher -> true/false Enable the watcher to monitor the snapshot file for changes during runtime
7778
switcher.snapshot.skipvalidation -> true/false Skip snapshotValidation() that can be used for UT executions
7879
switcher.snapshot.updateinterval -> Enable the Snapshot Auto Update given an interval of time - e.g. 1s (s: seconds, m: minutes)
7980
switcher.silent -> Enable contigency given the time for the client to retry - e.g. 5s (s: seconds - m: minutes - h: hours)
@@ -252,9 +253,9 @@ MyAppFeatures.scheduleSnapshotAutoUpdate("5s", new SnapshotCallback() {
252253
});
253254
```
254255

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

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

@@ -263,6 +264,15 @@ MyAppFeatures.watchSnapshot();
263264
MyAppFeatures.stopWatchingSnapshot();
264265
```
265266

267+
Alternatively, you can also set the Switcher Context configuration to start watching the snapshot file during the client initialization.
268+
269+
```java
270+
MyAppFeatures.configure(ContextBuilder.builder()
271+
.snapshotWatcher(true));
272+
273+
MyAppFeatures.initializeClient();
274+
```
275+
266276
2. You can also perform snapshot update validation to verify if there are changes to be pulled.
267277

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

320330
```java
321331
MyAppFeatures.configure(ContextBuilder.builder()
322-
...
323332
.checkSwitchers(true));
324333

325334
MyAppFeatures.initializeClient();

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@
5959

6060
<!-- test -->
6161
<okhttp.version>5.0.0-alpha.16</okhttp.version>
62-
<junit-jupiter.version>5.13.0</junit-jupiter.version>
62+
<junit-jupiter.version>5.13.1</junit-jupiter.version>
6363
<junit-pioneer.version>2.3.0</junit-pioneer.version>
64-
<junit-platform-launcher.version>1.13.0</junit-platform-launcher.version>
64+
<junit-platform-launcher.version>1.13.1</junit-platform-launcher.version>
6565

6666
<!-- Plugins -->
6767
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>

src/main/java/com/github/switcherapi/client/ContextBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ public ContextBuilder snapshotSkipValidation(boolean snapshotSkipValidation) {
147147
return this;
148148
}
149149

150+
/**
151+
* @param snapshotWatcher true/false When true, it will watch the snapshot file for changes and update the switchers accordingly
152+
* @return ContextBuilder
153+
*/
154+
public ContextBuilder snapshotWatcher(boolean snapshotWatcher) {
155+
switcherProperties.setValue(ContextKey.SNAPSHOT_WATCHER, snapshotWatcher);
156+
return this;
157+
}
158+
150159
/**
151160
* @param retryAfter Enable contingency given the time for the client to retry - e.g. 5s (s: seconds - m: minutes - h: hours)
152161
* @return ContextBuilder

src/main/java/com/github/switcherapi/client/SwitcherConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ protected void updateSwitcherConfig(SwitcherProperties properties) {
5050
snapshotConfig.setLocation(properties.getValue(ContextKey.SNAPSHOT_LOCATION));
5151
snapshotConfig.setAuto(properties.getBoolean(ContextKey.SNAPSHOT_AUTO_LOAD));
5252
snapshotConfig.setSkipValidation(properties.getBoolean(ContextKey.SNAPSHOT_SKIP_VALIDATION));
53+
snapshotConfig.setWatcher(properties.getBoolean(ContextKey.SNAPSHOT_WATCHER));
5354
snapshotConfig.setUpdateInterval(properties.getValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL));
5455
setSnapshot(snapshotConfig);
5556

@@ -143,6 +144,7 @@ public static class SnapshotConfig {
143144
private String location;
144145
private boolean auto;
145146
private boolean skipValidation;
147+
private boolean watcher;
146148
private String updateInterval;
147149

148150
public String getLocation() {
@@ -169,6 +171,14 @@ public void setSkipValidation(boolean skipValidation) {
169171
this.skipValidation = skipValidation;
170172
}
171173

174+
public boolean isWatcher() {
175+
return watcher;
176+
}
177+
178+
public void setWatcher(boolean watcher) {
179+
this.watcher = watcher;
180+
}
181+
172182
public String getUpdateInterval() {
173183
return updateInterval;
174184
}

src/main/java/com/github/switcherapi/client/SwitcherContextBase.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ protected void configureClient() {
114114
.poolConnectionSize(poolSize)
115115
.snapshotLocation(snapshot.getLocation())
116116
.snapshotAutoLoad(snapshot.isAuto())
117+
.snapshotWatcher(snapshot.isWatcher())
117118
.snapshotSkipValidation(snapshot.isSkipValidation())
118119
.snapshotAutoUpdateInterval(snapshot.getUpdateInterval())
119120
.truststorePath(truststore.getPath())
@@ -184,6 +185,7 @@ public static void initializeClient() {
184185
switcherExecutor = buildInstance();
185186

186187
loadSwitchers();
188+
scheduleSnapshotWatcher();
187189
scheduleSnapshotAutoUpdate(contextStr(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL));
188190
ContextBuilder.preConfigure(switcherProperties);
189191
SwitcherUtils.debug(logger, "Switcher Client initialized");
@@ -269,6 +271,18 @@ private static void loadSwitchers() throws SwitchersValidationException {
269271
}
270272
}
271273

274+
/**
275+
* Schedule a task to watch the snapshot file for modifications.<br>
276+
* The task will be executed in a single thread executor service.
277+
* <p>
278+
* (*) Requires client to use local settings
279+
*/
280+
private static void scheduleSnapshotWatcher() {
281+
if (contextBol(ContextKey.SNAPSHOT_WATCHER)) {
282+
watchSnapshot();
283+
}
284+
}
285+
272286
/**
273287
* Schedule a task to update the snapshot automatically.<br>
274288
* The task will be executed in a single thread executor service.
@@ -387,8 +401,8 @@ public static SwitcherRequest getSwitcher(String key) {
387401
}
388402

389403
/**
390-
* Validate if the snapshot version is the same as the one in the API.<br>
391-
* If the version is different, it will update the snapshot in memory.
404+
* Validate if the local snapshot version is the same as remote.<br>
405+
* If the version is different, it will update the local snapshot.
392406
*
393407
* @return true if snapshot was updated
394408
*/
@@ -403,7 +417,7 @@ public static boolean validateSnapshot() {
403417

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

415429
/**
416430
* Start watching snapshot files for modifications.<br>
417-
* When the file is modified the in-memory snapshot will reload
431+
* When the file is modified the local snapshot will reload
418432
*
419433
* <p>
420434
* (*) Requires client to use local settings

src/main/java/com/github/switcherapi/client/SwitcherPropertiesImpl.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,58 @@
22

33
import com.github.switcherapi.client.exception.SwitcherContextException;
44
import com.github.switcherapi.client.model.ContextKey;
5-
import com.github.switcherapi.client.utils.SwitcherUtils;
65
import org.apache.commons.lang3.StringUtils;
76

87
import java.util.HashMap;
98
import java.util.Map;
109
import java.util.Properties;
1110

1211
import static com.github.switcherapi.client.remote.Constants.*;
12+
import static com.github.switcherapi.client.utils.SwitcherUtils.*;
1313

1414
public class SwitcherPropertiesImpl implements SwitcherProperties {
1515

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

1818
public SwitcherPropertiesImpl() {
19+
initDefaults();
20+
}
21+
22+
private void initDefaults() {
1923
setValue(ContextKey.ENVIRONMENT, DEFAULT_ENV);
2024
setValue(ContextKey.REGEX_TIMEOUT, DEFAULT_REGEX_TIMEOUT);
2125
setValue(ContextKey.TIMEOUT_MS, DEFAULT_TIMEOUT);
2226
setValue(ContextKey.POOL_CONNECTION_SIZE, DEFAULT_POOL_SIZE);
2327
setValue(ContextKey.SNAPSHOT_AUTO_LOAD, false);
2428
setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, false);
29+
setValue(ContextKey.SNAPSHOT_WATCHER, false);
2530
setValue(ContextKey.LOCAL_MODE, false);
2631
setValue(ContextKey.CHECK_SWITCHERS, false);
2732
setValue(ContextKey.RESTRICT_RELAY, true);
2833
}
2934

3035
@Override
3136
public void loadFromProperties(Properties prop) {
32-
setValue(ContextKey.CONTEXT_LOCATION, SwitcherUtils.resolveProperties(ContextKey.CONTEXT_LOCATION.getParam(), prop));
33-
setValue(ContextKey.URL, SwitcherUtils.resolveProperties(ContextKey.URL.getParam(), prop));
34-
setValue(ContextKey.APIKEY, SwitcherUtils.resolveProperties(ContextKey.APIKEY.getParam(), prop));
35-
setValue(ContextKey.DOMAIN, SwitcherUtils.resolveProperties(ContextKey.DOMAIN.getParam(), prop));
36-
setValue(ContextKey.COMPONENT, SwitcherUtils.resolveProperties(ContextKey.COMPONENT.getParam(), prop));
37-
setValue(ContextKey.ENVIRONMENT, getValueDefault(SwitcherUtils.resolveProperties(ContextKey.ENVIRONMENT.getParam(), prop), DEFAULT_ENV));
38-
setValue(ContextKey.SNAPSHOT_LOCATION, SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_LOCATION.getParam(), prop));
39-
setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_SKIP_VALIDATION.getParam(), prop), false));
40-
setValue(ContextKey.SNAPSHOT_AUTO_LOAD, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_AUTO_LOAD.getParam(), prop), false));
41-
setValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL, SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL.getParam(), prop));
42-
setValue(ContextKey.SILENT_MODE, SwitcherUtils.resolveProperties(ContextKey.SILENT_MODE.getParam(), prop));
43-
setValue(ContextKey.LOCAL_MODE, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
44-
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
45-
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
46-
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
47-
setValue(ContextKey.TRUSTSTORE_PATH, SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
48-
setValue(ContextKey.TRUSTSTORE_PASSWORD, SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));
49-
setValue(ContextKey.TIMEOUT_MS, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.TIMEOUT_MS.getParam(), prop), DEFAULT_TIMEOUT));
50-
setValue(ContextKey.POOL_CONNECTION_SIZE, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.POOL_CONNECTION_SIZE.getParam(), prop), DEFAULT_POOL_SIZE));
37+
setValue(ContextKey.CONTEXT_LOCATION, resolveProperties(ContextKey.CONTEXT_LOCATION.getParam(), prop));
38+
setValue(ContextKey.URL, resolveProperties(ContextKey.URL.getParam(), prop));
39+
setValue(ContextKey.APIKEY, resolveProperties(ContextKey.APIKEY.getParam(), prop));
40+
setValue(ContextKey.DOMAIN, resolveProperties(ContextKey.DOMAIN.getParam(), prop));
41+
setValue(ContextKey.COMPONENT, resolveProperties(ContextKey.COMPONENT.getParam(), prop));
42+
setValue(ContextKey.ENVIRONMENT, getValueDefault(resolveProperties(ContextKey.ENVIRONMENT.getParam(), prop), DEFAULT_ENV));
43+
setValue(ContextKey.SNAPSHOT_LOCATION, resolveProperties(ContextKey.SNAPSHOT_LOCATION.getParam(), prop));
44+
setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, getBoolDefault(resolveProperties(ContextKey.SNAPSHOT_SKIP_VALIDATION.getParam(), prop), false));
45+
setValue(ContextKey.SNAPSHOT_AUTO_LOAD, getBoolDefault(resolveProperties(ContextKey.SNAPSHOT_AUTO_LOAD.getParam(), prop), false));
46+
setValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL, resolveProperties(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL.getParam(), prop));
47+
setValue(ContextKey.SNAPSHOT_WATCHER, getBoolDefault(resolveProperties(ContextKey.SNAPSHOT_WATCHER.getParam(), prop), false));
48+
setValue(ContextKey.SILENT_MODE, resolveProperties(ContextKey.SILENT_MODE.getParam(), prop));
49+
setValue(ContextKey.LOCAL_MODE, getBoolDefault(resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
50+
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
51+
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
52+
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
53+
setValue(ContextKey.TRUSTSTORE_PATH, resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
54+
setValue(ContextKey.TRUSTSTORE_PASSWORD, resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));
55+
setValue(ContextKey.TIMEOUT_MS, getIntDefault(resolveProperties(ContextKey.TIMEOUT_MS.getParam(), prop), DEFAULT_TIMEOUT));
56+
setValue(ContextKey.POOL_CONNECTION_SIZE, getIntDefault(resolveProperties(ContextKey.POOL_CONNECTION_SIZE.getParam(), prop), DEFAULT_POOL_SIZE));
5157
}
5258

5359
@Override

src/main/java/com/github/switcherapi/client/model/ContextKey.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public enum ContextKey {
5858
* (String) Interval given to the library to update the snapshot
5959
*/
6060
SNAPSHOT_AUTO_UPDATE_INTERVAL("switcher.snapshot.updateinterval"),
61+
62+
/**
63+
* (boolean) Defines if the client will watch the snapshot file for changes and update the switchers accordingly. (default is false)
64+
*/
65+
SNAPSHOT_WATCHER("switcher.snapshot.watcher"),
6166

6267
/**
6368
* (String) Defines if client will work in silent mode by specifying the time interval to retry
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.github.switcherapi.client.utils;
2+
3+
import com.github.switcherapi.SwitchersBase;
4+
import com.github.switcherapi.client.model.criteria.Data;
5+
import com.github.switcherapi.client.model.criteria.Domain;
6+
import com.github.switcherapi.client.model.criteria.Snapshot;
7+
import com.github.switcherapi.client.service.WorkerName;
8+
import com.google.gson.Gson;
9+
import com.google.gson.GsonBuilder;
10+
import org.apache.commons.lang3.StringUtils;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.io.BufferedWriter;
15+
import java.io.FileWriter;
16+
import java.io.IOException;
17+
import java.io.PrintWriter;
18+
import java.nio.file.Files;
19+
import java.nio.file.Paths;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
abstract class SnapshotTest {
24+
25+
private static final Logger logger = LoggerFactory.getLogger(SnapshotTest.class);
26+
27+
protected static final String SNAPSHOTS_LOCAL = Paths.get(StringUtils.EMPTY).toAbsolutePath() + "/src/test/resources";
28+
29+
protected static void removeGeneratedFiles() throws IOException {
30+
SwitchersBase.stopWatchingSnapshot();
31+
Files.deleteIfExists(Paths.get(SNAPSHOTS_LOCAL + "\\generated_watcher_default.json"));
32+
}
33+
34+
protected static void generateFixture() {
35+
final Snapshot mockedSnapshot = new Snapshot();
36+
final Data data = new Data();
37+
data.setDomain(SnapshotLoader.loadSnapshot(SNAPSHOTS_LOCAL + "/snapshot_watcher.json"));
38+
mockedSnapshot.setData(data);
39+
40+
SnapshotLoader.saveSnapshot(mockedSnapshot, SNAPSHOTS_LOCAL, "generated_watcher_default");
41+
}
42+
43+
protected void changeFixture() {
44+
final Snapshot mockedSnapshot = new Snapshot();
45+
final Data data = new Data();
46+
data.setDomain(SnapshotLoader.loadSnapshot(SNAPSHOTS_LOCAL + "/snapshot_watcher.json"));
47+
mockedSnapshot.setData(data);
48+
49+
data.setDomain(new Domain(
50+
data.getDomain().getName(),
51+
data.getDomain().getDescription(),
52+
!data.getDomain().isActivated(),
53+
data.getDomain().getVersion(),
54+
data.getDomain().getGroup()));
55+
56+
final Gson gson = new GsonBuilder().setPrettyPrinting().create();
57+
writeFixture(gson.toJson(mockedSnapshot));
58+
}
59+
60+
protected void writeFixture(String content) {
61+
try (
62+
final FileWriter fileWriter = new FileWriter(
63+
String.format("%s/%s.json", SNAPSHOTS_LOCAL, "generated_watcher_default"));
64+
65+
final BufferedWriter bw = new BufferedWriter(fileWriter);
66+
final PrintWriter wr = new PrintWriter(bw)) {
67+
wr.write(content);
68+
} catch (Exception e) {
69+
logger.error(e.getMessage(), e);
70+
}
71+
}
72+
73+
protected void assertWorker(boolean exists) {
74+
assertEquals(exists, Thread.getAllStackTraces().keySet().stream()
75+
.anyMatch(t -> t.getName().equals(WorkerName.SNAPSHOT_WATCH_WORKER.toString())));
76+
}
77+
}

0 commit comments

Comments
 (0)