Skip to content

Commit cd0e110

Browse files
chore: flagd add offline flag source path support through env variables (#647)
Signed-off-by: Kavindu Dodanduwa <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent e0b4e10 commit cd0e110

File tree

7 files changed

+53
-50
lines changed

7 files changed

+53
-50
lines changed

Diff for: providers/flagd/README.md

+20-13
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ FlagdProvider flagdProvider = new FlagdProvider(
4747

4848
In the above example, in-process handlers attempt to connect to a sync service on address `localhost:8013` to obtain [flag definitions](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json).
4949

50+
#### Offline mode
51+
5052
In-process resolvers can also work in an offline mode.
5153
To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`.
5254

@@ -58,9 +60,13 @@ FlagdProvider flagdProvider = new FlagdProvider(
5860
.build());
5961
```
6062

61-
Provider will not detect file changes nor re-read the file after the initial read.
62-
This mode is useful for local development, test cases, and offline applications.
63-
For a full-featured, production-ready file-based implementation, use the RPC evaluator in combination with the flagd standalone application, which can be configured to watch files for changes.
63+
Provider will attempt to detect file changes using polling.
64+
Polling happens at 5 second intervals and this is currently unconfigurable.
65+
This mode is useful for local development, tests and offline applications.
66+
67+
> [!IMPORTANT]
68+
> Note that you can only use a single flag source (either gRPC or offline file) for the in-process resolver.
69+
> If both sources are configured, offline mode will be selected.
6470
6571
### Configuration options
6672

@@ -73,17 +79,18 @@ Given below are the supported configurations:
7379

7480
| Option name | Environment variable name | Type & Values | Default | Compatible resolver |
7581
|-----------------------|--------------------------------|------------------------|-----------|---------------------|
76-
| host | FLAGD_HOST | String | localhost | RPC & in-process |
77-
| port | FLAGD_PORT | int | 8013 | RPC & in-process |
78-
| tls | FLAGD_TLS | boolean | false | RPC & in-process |
79-
| socketPath | FLAGD_SOCKET_PATH | String | null | RPC & in-process |
80-
| certPath | FLAGD_SERVER_CERT_PATH | String | null | RPC & in-process |
81-
| deadline | FLAGD_DEADLINE_MS | int | 500 | RPC & in-process |
82+
| host | FLAGD_HOST | String | localhost | rpc & in-process |
83+
| port | FLAGD_PORT | int | 8013 | rpc & in-process |
84+
| tls | FLAGD_TLS | boolean | false | rpc & in-process |
85+
| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process |
86+
| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process |
87+
| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process |
8288
| selector | FLAGD_SOURCE_SELECTOR | String | null | in-process |
83-
| cache | FLAGD_CACHE | String - lru, disabled | lru | RPC |
84-
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | RPC |
85-
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | RPC |
86-
| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | RPC |
89+
| cache | FLAGD_CACHE | String - lru, disabled | lru | rpc |
90+
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc |
91+
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc |
92+
| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc |
93+
| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | String | null | in-process |
8794

8895
> [!NOTE]
8996
> Some configurations are only applicable for RPC resolver.

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

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public final class Config {
2525
static final String BASE_EVENT_STREAM_RETRY_BACKOFF_MS_ENV_VAR_NAME = "FLAGD_RETRY_BACKOFF_MS";
2626
static final String DEADLINE_MS_ENV_VAR_NAME = "FLAGD_DEADLINE_MS";
2727
static final String SOURCE_SELECTOR_ENV_VAR_NAME = "FLAGD_SOURCE_SELECTOR";
28+
static final String OFFLINE_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH";
2829

2930
public static final String STATIC_REASON = "STATIC";
3031
public static final String CACHED_REASON = "CACHED";

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

+6-28
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import lombok.Builder;
66
import lombok.Getter;
77

8-
import javax.annotation.Nonnull;
9-
108
import static dev.openfeature.contrib.providers.flagd.Config.BASE_EVENT_STREAM_RETRY_BACKOFF_MS;
119
import static dev.openfeature.contrib.providers.flagd.Config.BASE_EVENT_STREAM_RETRY_BACKOFF_MS_ENV_VAR_NAME;
1210
import static dev.openfeature.contrib.providers.flagd.Config.CACHE_ENV_VAR_NAME;
@@ -22,6 +20,7 @@
2220
import static dev.openfeature.contrib.providers.flagd.Config.HOST_ENV_VAR_NAME;
2321
import static dev.openfeature.contrib.providers.flagd.Config.MAX_CACHE_SIZE_ENV_VAR_NAME;
2422
import static dev.openfeature.contrib.providers.flagd.Config.MAX_EVENT_STREAM_RETRIES_ENV_VAR_NAME;
23+
import static dev.openfeature.contrib.providers.flagd.Config.OFFLINE_SOURCE_PATH;
2524
import static dev.openfeature.contrib.providers.flagd.Config.PORT_ENV_VAR_NAME;
2625
import static dev.openfeature.contrib.providers.flagd.Config.SERVER_CERT_PATH_ENV_VAR_NAME;
2726
import static dev.openfeature.contrib.providers.flagd.Config.SOCKET_PATH_ENV_VAR_NAME;
@@ -118,12 +117,8 @@ public class FlagdOptions {
118117
* File source of flags to be used by offline mode.
119118
* Setting this enables the offline mode of the in-process provider.
120119
*/
121-
private String offlineFlagSourcePath;
122-
123-
/**
124-
* Flagd option to state the offline mode. Only get set with offlineFlagSourcePath.
125-
*/
126-
private boolean isOffline;
120+
@Builder.Default
121+
private String offlineFlagSourcePath = fallBackToEnvOrDefault(OFFLINE_SOURCE_PATH, null);
127122

128123
/**
129124
* Inject OpenTelemetry for the library runtime. Providing sdk will initiate distributed tracing for flagd grpc
@@ -135,31 +130,14 @@ public class FlagdOptions {
135130
* Overload default lombok builder.
136131
*/
137132
public static class FlagdOptionsBuilder {
138-
139-
/**
140-
* File source of flags to be used by offline mode.
141-
* Setting this enables the offline mode of the in-process provider.
142-
*/
143-
public FlagdOptionsBuilder offlineFlagSourcePath(@Nonnull final String offlineFlagSourcePath) {
144-
this.isOffline = true;
145-
this.offlineFlagSourcePath = offlineFlagSourcePath;
146-
147-
return this;
148-
}
149-
150-
// Remove the public access as this needs to be connected to offlineFlagSourcePath
151-
@SuppressWarnings({"PMD.UnusedFormalParameter", "PMD.UnusedPrivateMethod"})
152-
private FlagdOptionsBuilder isOffline(final boolean isOffline) {
153-
return this;
154-
}
155-
156133
/**
157134
* Enable OpenTelemetry instance extraction from GlobalOpenTelemetry. Note that, this is only useful if global
158135
* configurations are registered.
159136
*/
160137
public FlagdOptionsBuilder withGlobalTelemetry(final boolean b) {
161-
this.openTelemetry = GlobalOpenTelemetry.get();
162-
138+
if (b) {
139+
this.openTelemetry = GlobalOpenTelemetry.get();
140+
}
163141
return this;
164142
}
165143
}

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ public class InProcessResolver implements Resolver {
4646
* Initialize an in-process resolver.
4747
*/
4848
public InProcessResolver(FlagdOptions options, Consumer<ProviderState> stateConsumer) {
49-
final Connector connector = options.isOffline()
50-
? new FileConnector(options.getOfflineFlagSourcePath())
51-
: new GrpcStreamConnector(options);
52-
53-
this.flagStore = new FlagStore(connector);
49+
this.flagStore = new FlagStore(getConnector(options));
5450
this.deadline = options.getDeadline();
5551
this.stateConsumer = stateConsumer;
5652
this.operator = new Operator();
@@ -153,6 +149,12 @@ public ProviderEvaluation<Value> objectEvaluation(String key, Value defaultValue
153149
.build();
154150
}
155151

152+
static Connector getConnector(final FlagdOptions options) {
153+
return options.getOfflineFlagSourcePath() != null && !options.getOfflineFlagSourcePath().isEmpty()
154+
? new FileConnector(options.getOfflineFlagSourcePath())
155+
: new GrpcStreamConnector(options);
156+
}
157+
156158
private <T> ProviderEvaluation<T> resolve(Class<T> type, String key,
157159
EvaluationContext ctx) {
158160
final FeatureFlag flag = flagStore.getFlag(key);

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import static org.junit.jupiter.api.Assertions.assertNull;
1515
import static org.junit.jupiter.api.Assertions.assertTrue;
1616

17-
public class FeatureFlagProviderBuilderTest {
17+
public class FlagdOptionsTest {
1818

1919
@Test
2020
public void TestDefaults() {
@@ -31,7 +31,6 @@ public void TestDefaults() {
3131
assertNull(builder.getSelector());
3232
assertNull(builder.getOpenTelemetry());
3333
assertNull(builder.getOfflineFlagSourcePath());
34-
assertFalse(builder.isOffline());
3534
}
3635

3736
@Test
@@ -59,7 +58,6 @@ public void TestBuilderOptions() {
5958
assertEquals(flagdOptions.getMaxCacheSize(), 100);
6059
assertEquals(flagdOptions.getMaxEventStreamRetries(), 1);
6160
assertEquals(flagdOptions.getSelector(), "app=weatherApp");
62-
assertTrue(flagdOptions.isOffline());
6361
assertEquals("some-path", flagdOptions.getOfflineFlagSourcePath());
6462
assertEquals(flagdOptions.getOpenTelemetry(), openTelemetry);
6563
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class StepDefinitions {
4545
* Tests run one at a time, but just in case, a lock is used to make sure the
4646
* client is not updated mid-test.
4747
*
48-
* @param client client to inject into test.
48+
* @param provider client to inject into test.
4949
*/
5050
public static void setProvider(FeatureProvider provider) {
5151
StepDefinitions.provider = provider;

Diff for: providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java

+17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package dev.openfeature.contrib.providers.flagd.resolver.process;
22

3+
import dev.openfeature.contrib.providers.flagd.Config;
34
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
45
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
56
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState;
7+
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file.FileConnector;
8+
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.grpc.GrpcStreamConnector;
69
import dev.openfeature.sdk.ImmutableContext;
710
import dev.openfeature.sdk.ImmutableMetadata;
811
import dev.openfeature.sdk.MutableContext;
@@ -35,12 +38,26 @@
3538
import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.OBJECT_FLAG;
3639
import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.VARIANT_MISMATCH_FLAG;
3740
import static org.junit.jupiter.api.Assertions.assertEquals;
41+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
3842
import static org.junit.jupiter.api.Assertions.assertNotNull;
3943
import static org.junit.jupiter.api.Assertions.assertThrows;
4044
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
4145

4246
class InProcessResolverTest {
4347

48+
@Test
49+
public void connectorSetup(){
50+
// given
51+
FlagdOptions forGrpcOptions =
52+
FlagdOptions.builder().resolverType(Config.Evaluator.IN_PROCESS).host("localhost").port(8080).build();
53+
FlagdOptions forOfflineOptions =
54+
FlagdOptions.builder().resolverType(Config.Evaluator.IN_PROCESS).offlineFlagSourcePath("path").build();
55+
56+
// then
57+
assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions));
58+
assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions));
59+
}
60+
4461
@Test
4562
public void eventHandling() throws Throwable {
4663
// given

0 commit comments

Comments
 (0)