Skip to content

Commit 1bd8f86

Browse files
authored
feat(flagd): migrate file to own provider type (#1173)
Signed-off-by: Simon Schrottner <[email protected]>
1 parent e1f2bc3 commit 1bd8f86

File tree

17 files changed

+149
-70
lines changed

17 files changed

+149
-70
lines changed

Diff for: providers/flagd/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tmp/

Diff for: providers/flagd/README.md

+21-20
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ The value is updated with every (re)connection to the sync implementation.
5454
This can be used to enrich evaluations with such data.
5555
If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map.
5656

57-
#### Offline mode
57+
### Offline mode (File resolver)
5858

5959
In-process resolvers can also work in an offline mode.
6060
To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`.
6161

6262
```java
6363
FlagdProvider flagdProvider = new FlagdProvider(
6464
FlagdOptions.builder()
65-
.resolverType(Config.Resolver.IN_PROCESS)
65+
.resolverType(Config.Resolver.FILE)
6666
.offlineFlagSourcePath("PATH")
6767
.build());
6868
```
@@ -103,24 +103,25 @@ variables.
103103

104104
Given below are the supported configurations:
105105

106-
| Option name | Environment variable name | Type & Values | Default | Compatible resolver |
107-
| --------------------- | ------------------------------ | ------------------------ | --------- | ------------------- |
108-
| resolver | FLAGD_RESOLVER | String - rpc, in-process | rpc | |
109-
| host | FLAGD_HOST | String | localhost | rpc & in-process |
110-
| port | FLAGD_PORT | int | 8013 | rpc & in-process |
111-
| targetUri | FLAGD_TARGET_URI | string | null | rpc & in-process |
112-
| tls | FLAGD_TLS | boolean | false | rpc & in-process |
113-
| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process |
114-
| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process |
115-
| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process |
116-
| streamDeadlineMs | FLAGD_STREAM_DEADLINE_MS | int | 600000 | rpc & in-process |
117-
| keepAliveTime | FLAGD_KEEP_ALIVE_TIME_MS | long | 0 | rpc & in-process |
118-
| selector | FLAGD_SOURCE_SELECTOR | String | null | in-process |
119-
| cache | FLAGD_CACHE | String - lru, disabled | lru | rpc |
120-
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc |
121-
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc |
122-
| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc |
123-
| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | String | null | in-process |
106+
| Option name | Environment variable name | Type & Values | Default | Compatible resolver |
107+
|-----------------------|--------------------------------|--------------------------|-----------|-------------------------|
108+
| resolver | FLAGD_RESOLVER | String - rpc, in-process | rpc | |
109+
| host | FLAGD_HOST | String | localhost | rpc & in-process |
110+
| port | FLAGD_PORT | int | 8013 | rpc & in-process |
111+
| targetUri | FLAGD_TARGET_URI | string | null | rpc & in-process |
112+
| tls | FLAGD_TLS | boolean | false | rpc & in-process |
113+
| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process |
114+
| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process |
115+
| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process & file |
116+
| streamDeadlineMs | FLAGD_STREAM_DEADLINE_MS | int | 600000 | rpc & in-process |
117+
| keepAliveTime | FLAGD_KEEP_ALIVE_TIME_MS | long | 0 | rpc & in-process |
118+
| selector | FLAGD_SOURCE_SELECTOR | String | null | in-process |
119+
| cache | FLAGD_CACHE | String - lru, disabled | lru | rpc |
120+
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc |
121+
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc |
122+
| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc |
123+
| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | String | null | file |
124+
| offlinePollIntervalMs | FLAGD_OFFLINE_POLL_MS | int | 5000 | file |
124125

125126
> [!NOTE]
126127
> Some configurations are only applicable for RPC resolver.

Diff for: providers/flagd/schemas

Diff for: providers/flagd/spec

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public final class Config {
1717
static final int DEFAULT_STREAM_DEADLINE_MS = 10 * 60 * 1000;
1818
static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 5;
1919
static final int DEFAULT_MAX_CACHE_SIZE = 1000;
20+
static final int DEFAULT_OFFLINE_POLL_MS = 5000;
2021
static final long DEFAULT_KEEP_ALIVE = 0;
2122

2223
static final String RESOLVER_ENV_VAR = "FLAGD_RESOLVER";
@@ -33,13 +34,15 @@ public final class Config {
3334
static final String STREAM_DEADLINE_MS_ENV_VAR_NAME = "FLAGD_STREAM_DEADLINE_MS";
3435
static final String SOURCE_SELECTOR_ENV_VAR_NAME = "FLAGD_SOURCE_SELECTOR";
3536
static final String OFFLINE_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH";
37+
static final String OFFLINE_POLL_MS = "FLAGD_OFFLINE_POLL_MS";
3638
static final String KEEP_ALIVE_MS_ENV_VAR_NAME_OLD = "FLAGD_KEEP_ALIVE_TIME";
3739
static final String KEEP_ALIVE_MS_ENV_VAR_NAME = "FLAGD_KEEP_ALIVE_TIME_MS";
3840
static final String TARGET_URI_ENV_VAR_NAME = "FLAGD_TARGET_URI";
3941
static final String STREAM_RETRY_GRACE_PERIOD = "FLAGD_RETRY_GRACE_PERIOD";
4042

4143
static final String RESOLVER_RPC = "rpc";
4244
static final String RESOLVER_IN_PROCESS = "in-process";
45+
static final String RESOLVER_FILE = "file";
4346

4447
public static final String STATIC_REASON = "STATIC";
4548
public static final String CACHED_REASON = "CACHED";
@@ -87,6 +90,8 @@ static Resolver fromValueProvider(Function<String, String> provider) {
8790
return Resolver.IN_PROCESS;
8891
case "rpc":
8992
return Resolver.RPC;
93+
case "file":
94+
return Resolver.FILE;
9095
default:
9196
log.warn("Unsupported resolver variable: {}", resolverVar);
9297
return DEFAULT_RESOLVER_TYPE;
@@ -143,6 +148,11 @@ public String asString() {
143148
public String asString() {
144149
return RESOLVER_IN_PROCESS;
145150
}
151+
},
152+
FILE {
153+
public String asString() {
154+
return RESOLVER_FILE;
155+
}
146156
}
147157
}
148158
}

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.function.Function;
1313
import lombok.Builder;
1414
import lombok.Getter;
15+
import org.apache.commons.lang3.StringUtils;
1516

1617
/**
1718
* FlagdOptions is a builder to build flagd provider options.
@@ -119,8 +120,14 @@ public class FlagdOptions {
119120
* File source of flags to be used by offline mode.
120121
* Setting this enables the offline mode of the in-process provider.
121122
*/
123+
private String offlineFlagSourcePath;
124+
125+
/**
126+
* File polling interval.
127+
* Defaults to 0 (disabled).
128+
**/
122129
@Builder.Default
123-
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);
130+
private int offlinePollIntervalMs = fallBackToEnvOrDefault(Config.OFFLINE_POLL_MS, Config.DEFAULT_OFFLINE_POLL_MS);
124131

125132
/**
126133
* gRPC custom target string.
@@ -193,7 +200,20 @@ void prebuild() {
193200
resolverType = fromValueProvider(System::getenv);
194201
}
195202

196-
if (port == 0) {
203+
if (StringUtils.isBlank(offlineFlagSourcePath)) {
204+
offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);
205+
}
206+
207+
if (!StringUtils.isEmpty(offlineFlagSourcePath) && resolverType == Config.Resolver.IN_PROCESS) {
208+
resolverType = Config.Resolver.FILE;
209+
}
210+
211+
// We need a file path for FILE Provider
212+
if (StringUtils.isEmpty(offlineFlagSourcePath) && resolverType == Config.Resolver.FILE) {
213+
throw new IllegalArgumentException("Resolver Type 'FILE' requires a offlineFlagSourcePath");
214+
}
215+
216+
if (port == 0 && resolverType != Config.Resolver.FILE) {
197217
port = Integer.parseInt(
198218
fallBackToEnvOrDefault(Config.PORT_ENV_VAR_NAME, determineDefaultPortForResolver()));
199219
}

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public FlagdProvider() {
8080
*/
8181
public FlagdProvider(final FlagdOptions options) {
8282
switch (options.getResolverType().asString()) {
83+
case Config.RESOLVER_FILE:
8384
case Config.RESOLVER_IN_PROCESS:
8485
this.flagResolver = new InProcessResolver(options, this::onProviderEvent);
8586
break;

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ public GrpcResolver(
7272
Evaluation.EventStreamRequest.getDefaultInstance(),
7373
new EventStreamObserver(
7474
(flags) -> {
75-
if (cache != null) {
76-
flags.forEach(cache::remove);
75+
if (this.cache != null) {
76+
flags.forEach(this.cache::remove);
7777
}
7878
onProviderEvent.accept(new FlagdProviderEvent(
7979
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, flags));

Diff for: providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ static Connector getConnector(final FlagdOptions options, Consumer<FlagdProvider
151151
}
152152
return options.getOfflineFlagSourcePath() != null
153153
&& !options.getOfflineFlagSourcePath().isEmpty()
154-
? new FileConnector(options.getOfflineFlagSourcePath())
154+
? new FileConnector(options.getOfflineFlagSourcePath(), options.getOfflinePollIntervalMs())
155155
: new GrpcStreamConnector(options, onConnectionEvent);
156156
}
157157

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@
2424
@Slf4j
2525
public class FileConnector implements Connector {
2626

27-
private static final int POLL_INTERVAL_MS = 5000;
2827
private static final String OFFER_WARN = "Unable to offer file content to queue: queue is full";
2928

3029
private final String flagSourcePath;
30+
private final int pollInterval;
3131
private final BlockingQueue<QueuePayload> queue = new LinkedBlockingQueue<>(1);
3232
private boolean shutdown = false;
3333

34-
public FileConnector(final String flagSourcePath) {
34+
public FileConnector(final String flagSourcePath, int pollInterval) {
3535
this.flagSourcePath = flagSourcePath;
36+
this.pollInterval = pollInterval;
3637
}
3738

3839
/**
@@ -64,7 +65,7 @@ public void init() throws IOException {
6465
}
6566
}
6667

67-
Thread.sleep(POLL_INTERVAL_MS);
68+
Thread.sleep(pollInterval);
6869
}
6970

7071
log.info("Shutting down file connector.");

Diff for: providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java

-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ void TestBuilderOptions() {
6161
.cacheType("lru")
6262
.maxCacheSize(100)
6363
.selector("app=weatherApp")
64-
.offlineFlagSourcePath("some-path")
6564
.openTelemetry(openTelemetry)
6665
.customConnector(connector)
6766
.resolverType(Resolver.IN_PROCESS)
@@ -76,7 +75,6 @@ void TestBuilderOptions() {
7675
assertEquals("lru", flagdOptions.getCacheType());
7776
assertEquals(100, flagdOptions.getMaxCacheSize());
7877
assertEquals("app=weatherApp", flagdOptions.getSelector());
79-
assertEquals("some-path", flagdOptions.getOfflineFlagSourcePath());
8078
assertEquals(openTelemetry, flagdOptions.getOpenTelemetry());
8179
assertEquals(connector, flagdOptions.getCustomConnector());
8280
assertEquals(Resolver.IN_PROCESS, flagdOptions.getResolverType());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dev.openfeature.contrib.providers.flagd.e2e;
2+
3+
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
4+
import static io.cucumber.junit.platform.engine.Constants.OBJECT_FACTORY_PROPERTY_NAME;
5+
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
6+
7+
import dev.openfeature.contrib.providers.flagd.Config;
8+
import org.apache.logging.log4j.core.config.Order;
9+
import org.junit.platform.suite.api.BeforeSuite;
10+
import org.junit.platform.suite.api.ConfigurationParameter;
11+
import org.junit.platform.suite.api.ExcludeTags;
12+
import org.junit.platform.suite.api.IncludeEngines;
13+
import org.junit.platform.suite.api.IncludeTags;
14+
import org.junit.platform.suite.api.SelectDirectories;
15+
import org.junit.platform.suite.api.Suite;
16+
import org.testcontainers.junit.jupiter.Testcontainers;
17+
18+
/**
19+
* Class for running the reconnection tests for the RPC provider
20+
*/
21+
@Order(value = Integer.MAX_VALUE)
22+
@Suite
23+
@IncludeEngines("cucumber")
24+
@SelectDirectories("test-harness/gherkin")
25+
// if you want to run just one feature file, use the following line instead of @SelectDirectories
26+
// @SelectFile("test-harness/gherkin/connection.feature")
27+
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
28+
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps")
29+
@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory")
30+
@IncludeTags("file")
31+
@ExcludeTags({"unixsocket", "targetURI", "reconnect", "customCert"})
32+
@Testcontainers
33+
public class RunFileTest {
34+
35+
@BeforeSuite
36+
public static void before() {
37+
State.resolverType = Config.Resolver.FILE;
38+
}
39+
}

Diff for: providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java

+1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ public class State {
2525
public FlagdOptions options;
2626
public FlagdOptions.FlagdOptionsBuilder builder = FlagdOptions.builder();
2727
public static Config.Resolver resolverType;
28+
public boolean hasError;
2829
}

Diff for: providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java

+19-24
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import static io.restassured.RestAssured.when;
44

5-
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import com.fasterxml.jackson.databind.ObjectReader;
75
import dev.openfeature.contrib.providers.flagd.Config;
86
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
97
import dev.openfeature.contrib.providers.flagd.e2e.FlagdContainer;
@@ -21,7 +19,6 @@
2119
import java.nio.file.Files;
2220
import java.nio.file.Path;
2321
import java.nio.file.Paths;
24-
import java.util.Objects;
2522
import lombok.extern.slf4j.Slf4j;
2623
import org.apache.commons.lang3.RandomStringUtils;
2724
import org.junit.jupiter.api.parallel.Isolated;
@@ -47,7 +44,7 @@ public static void beforeAll() throws IOException {
4744
sharedTempDir = Files.createDirectories(
4845
Paths.get("tmp/" + RandomStringUtils.randomAlphanumeric(8).toLowerCase() + "/"));
4946
container = new FlagdContainer()
50-
.withFileSystemBind(sharedTempDir.toAbsolutePath().toString(), "/tmp", BindMode.READ_WRITE);
47+
.withFileSystemBind(sharedTempDir.toAbsolutePath().toString(), "/flags", BindMode.READ_WRITE);
5148
}
5249

5350
@AfterAll
@@ -78,10 +75,15 @@ public void setupProvider(String providerType) throws IOException, InterruptedEx
7875
String flagdConfig = "default";
7976
state.builder.deadline(1000).keepAlive(0).retryGracePeriod(2);
8077
boolean wait = true;
78+
8179
switch (providerType) {
8280
case "unavailable":
8381
this.state.providerType = ProviderType.SOCKET;
8482
state.builder.port(UNAVAILABLE_PORT);
83+
if (State.resolverType == Config.Resolver.FILE) {
84+
85+
state.builder.offlineFlagSourcePath("not-existing");
86+
}
8587
wait = false;
8688
break;
8789
case "socket":
@@ -103,34 +105,28 @@ public void setupProvider(String providerType) throws IOException, InterruptedEx
103105
.certPath(absolutePath);
104106
flagdConfig = "ssl";
105107
break;
106-
case "offline":
107-
State.resolverType = Config.Resolver.IN_PROCESS;
108-
File flags = new File("test-harness/flags");
109-
ObjectMapper objectMapper = new ObjectMapper();
110-
Object merged = new Object();
111-
for (File listFile : Objects.requireNonNull(flags.listFiles())) {
112-
ObjectReader updater = objectMapper.readerForUpdating(merged);
113-
merged = updater.readValue(listFile, Object.class);
114-
}
115-
Path offlinePath = Files.createTempFile("flags", ".json");
116-
objectMapper.writeValue(offlinePath.toFile(), merged);
117-
118-
state.builder
119-
.port(UNAVAILABLE_PORT)
120-
.offlineFlagSourcePath(offlinePath.toAbsolutePath().toString());
121-
break;
122108

123109
default:
124110
this.state.providerType = ProviderType.DEFAULT;
125-
state.builder.port(container.getPort(State.resolverType));
111+
if (State.resolverType == Config.Resolver.FILE) {
112+
113+
state.builder
114+
.port(UNAVAILABLE_PORT)
115+
.offlineFlagSourcePath(sharedTempDir
116+
.resolve("allFlags.json")
117+
.toAbsolutePath()
118+
.toString());
119+
} else {
120+
state.builder.port(container.getPort(State.resolverType));
121+
}
126122
break;
127123
}
128124
when().post("http://" + container.getLaunchpadUrl() + "/start?config={config}", flagdConfig)
129125
.then()
130126
.statusCode(200);
131127

132128
// giving flagd a little time to start
133-
Thread.sleep(100);
129+
Thread.sleep(30);
134130
FeatureProvider provider =
135131
new FlagdProvider(state.builder.resolverType(State.resolverType).build());
136132

@@ -159,10 +155,9 @@ public void the_connection_is_lost_for(int seconds) throws InterruptedException
159155

160156
@When("the flag was modified")
161157
public void the_flag_was_modded() throws InterruptedException {
162-
163158
when().post("http://" + container.getLaunchpadUrl() + "/change").then().statusCode(200);
164159

165160
// we might be too fast in the execution
166-
Thread.sleep(100);
161+
Thread.sleep(1000);
167162
}
168163
}

Diff for: providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public static Object convert(String value, String type) throws ClassNotFoundExce
3030
return Config.Resolver.IN_PROCESS;
3131
case "rpc":
3232
return Config.Resolver.RPC;
33+
case "file":
34+
return Config.Resolver.FILE;
3335
default:
3436
throw new RuntimeException("Unknown resolver type: " + value);
3537
}

0 commit comments

Comments
 (0)