Skip to content

Commit 5e97b12

Browse files
feat: flagd file polling for offline mode (#614)
Signed-off-by: Kavindu Dodanduwa <[email protected]>
1 parent 178fd42 commit 5e97b12

File tree

4 files changed

+115
-11
lines changed

4 files changed

+115
-11
lines changed

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnector.java

+45-8
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,73 @@
99
import java.io.IOException;
1010
import java.nio.charset.StandardCharsets;
1111
import java.nio.file.Files;
12+
import java.nio.file.Path;
1213
import java.nio.file.Paths;
1314
import java.util.concurrent.BlockingQueue;
1415
import java.util.concurrent.LinkedBlockingQueue;
1516

1617
/**
17-
* File connector reads flag configurations and expose the context through {@code Connector} contract.
18+
* File connector reads flag configurations from a given file, polls for changes and expose the content through
19+
* {@code Connector} contract.
1820
* The implementation is kept minimal and suites testing, local development needs.
1921
*/
2022
@SuppressFBWarnings(value = {"EI_EXPOSE_REP", "PATH_TRAVERSAL_IN"},
2123
justification = "File connector read feature flag from a file source.")
2224
@Slf4j
2325
public class FileConnector implements Connector {
2426

27+
private static final int POLL_INTERVAL_MS = 5000;
28+
2529
private final String flagSourcePath;
2630
private final BlockingQueue<StreamPayload> queue = new LinkedBlockingQueue<>(1);
31+
private boolean shutdown = false;
2732

2833
public FileConnector(final String flagSourcePath) {
2934
this.flagSourcePath = flagSourcePath;
3035
}
3136

3237
/**
33-
* Initialize file connector. Reads content of the provided source file and offer it through queue.
38+
* Initialize file connector. Reads file content, poll for changes and offer content through the queue.
3439
*/
3540
public void init() throws IOException {
36-
final String flagData = new String(Files.readAllBytes(Paths.get(flagSourcePath)), StandardCharsets.UTF_8);
41+
Thread watcherT = new Thread(() -> {
42+
try {
43+
final Path filePath = Paths.get(flagSourcePath);
44+
45+
// initial read
46+
String flagData = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
47+
if (!queue.offer(new StreamPayload(StreamPayloadType.DATA, flagData))) {
48+
log.warn("Unable to offer file content to queue: queue is full");
49+
}
50+
51+
long lastTS = Files.getLastModifiedTime(filePath).toMillis();
52+
53+
// start polling for changes
54+
while (!shutdown) {
55+
long currentTS = Files.getLastModifiedTime(filePath).toMillis();
56+
57+
if (currentTS > lastTS) {
58+
lastTS = currentTS;
59+
flagData = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
60+
if (!queue.offer(new StreamPayload(StreamPayloadType.DATA, flagData))) {
61+
log.warn("Unable to offer file content to queue: queue is full");
62+
}
63+
}
64+
65+
Thread.sleep(POLL_INTERVAL_MS);
66+
}
3767

38-
if (!queue.offer(new StreamPayload(StreamPayloadType.DATA, flagData))) {
39-
throw new RuntimeException("Unable to write to queue. Queue is full.");
40-
}
68+
log.info("Shutting down file connector.");
69+
} catch (Throwable t) {
70+
log.error("Error from file connector. File connector will exit", t);
71+
if (!queue.offer(new StreamPayload(StreamPayloadType.ERROR, t.toString()))) {
72+
log.warn("Unable to offer file content to queue: queue is full");
73+
}
74+
}
75+
});
4176

77+
watcherT.setDaemon(true);
78+
watcherT.start();
4279
log.info(String.format("Using feature flag configurations from file %s", flagSourcePath));
4380
}
4481

@@ -50,9 +87,9 @@ public BlockingQueue<StreamPayload> getStream() {
5087
}
5188

5289
/**
53-
* NO-OP shutdown.
90+
* Shutdown file connector.
5491
*/
5592
public void shutdown() throws InterruptedException {
56-
// NO-OP nothing to do here
93+
shutdown = true;
5794
}
5895
}

providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class TestUtils {
1313
public static final String VALID_LONG = "flagConfigurations/valid-long.json";
1414
public static final String INVALID_FLAG = "flagConfigurations/invalid-flag.json";
1515
public static final String INVALID_CFG = "flagConfigurations/invalid-configuration.json";
16+
public static final String UPDATABLE_FILE = "flagConfigurations/updatableFlags.json";
1617

1718

1819
public static String getFlagsFromResource(final String file) throws IOException {

providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/file/FileConnectorTest.java

+57-3
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayload;
44
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.StreamPayloadType;
5+
import org.junit.jupiter.api.Disabled;
56
import org.junit.jupiter.api.Test;
67

78
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
import java.nio.file.StandardOpenOption;
813
import java.time.Duration;
914
import java.util.concurrent.BlockingQueue;
1015

16+
import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.UPDATABLE_FILE;
1117
import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG;
1218
import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.getResourcePath;
1319
import static org.junit.jupiter.api.Assertions.assertEquals;
1420
import static org.junit.jupiter.api.Assertions.assertNotNull;
15-
import static org.junit.jupiter.api.Assertions.assertThrows;
1621
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
1722

1823
class FileConnectorTest {
@@ -39,12 +44,61 @@ void readAndExposeFeatureFlagsFromSource() throws IOException {
3944
}
4045

4146
@Test
42-
void throwsErrorIfInvalidFile(){
47+
void emitErrorStateForInvalidPath() throws IOException {
4348
// given
4449
final FileConnector connector = new FileConnector("INVALID_PATH");
4550

51+
// when
52+
connector.init();
53+
54+
// then
55+
final BlockingQueue<StreamPayload> stream = connector.getStream();
56+
57+
// Must emit an error within considerable time
58+
final StreamPayload[] payload = new StreamPayload[1];
59+
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
60+
payload[0] = stream.take();
61+
});
62+
63+
assertNotNull(payload[0].getData());
64+
assertEquals(StreamPayloadType.ERROR, payload[0].getType());
65+
}
66+
67+
@Test
68+
@Disabled("Disabled as unstable on GH Action. Useful for functionality validation")
69+
void watchForFileUpdatesAndEmitThem() throws IOException {
70+
final String initial = "{\"flags\":{\"myBoolFlag\":{\"state\":\"ENABLED\",\"variants\":{\"on\":true,\"off\":false},\"defaultVariant\":\"on\"}}}";
71+
final String updatedFlags = "{\"flags\":{\"myBoolFlag\":{\"state\":\"ENABLED\",\"variants\":{\"on\":true,\"off\":false},\"defaultVariant\":\"off\"}}}";
72+
73+
// given
74+
final Path updPath = Paths.get(getResourcePath(UPDATABLE_FILE));
75+
Files.write(updPath, initial.getBytes(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
76+
77+
final FileConnector connector = new FileConnector(updPath.toString());
78+
79+
// when
80+
connector.init();
81+
4682
// then
47-
assertThrows(IOException.class, connector::init);
83+
final BlockingQueue<StreamPayload> stream = connector.getStream();
84+
final StreamPayload[] payload = new StreamPayload[1];
85+
86+
// first validate the initial payload
87+
assertTimeoutPreemptively(Duration.ofMillis(200), () -> {
88+
payload[0] = stream.take();
89+
});
90+
91+
assertEquals(initial, payload[0].getData());
92+
93+
// then update the flags
94+
Files.write(updPath, updatedFlags.getBytes(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
95+
96+
// finally wait for updated payload
97+
assertTimeoutPreemptively(Duration.ofSeconds(10), () -> {
98+
payload[0] = stream.take();
99+
});
100+
101+
assertEquals(updatedFlags, payload[0].getData());
48102
}
49103

50104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"flags": {
3+
"myBoolFlag": {
4+
"state": "ENABLED",
5+
"variants": {
6+
"on": true,
7+
"off": false
8+
},
9+
"defaultVariant": "on"
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)