diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 9085ab16f..317b4ac53 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -47,6 +47,8 @@ FlagdProvider flagdProvider = new FlagdProvider( 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). +#### Offline mode + In-process resolvers can also work in an offline mode. To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`. @@ -58,9 +60,13 @@ FlagdProvider flagdProvider = new FlagdProvider( .build()); ``` -Provider will not detect file changes nor re-read the file after the initial read. -This mode is useful for local development, test cases, and offline applications. -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. +Provider will attempt to detect file changes using polling. +Polling happens at 5 second intervals and this is currently unconfigurable. +This mode is useful for local development, tests and offline applications. + +> [!IMPORTANT] +> Note that you can only use a single flag source (either gRPC or offline file) for the in-process resolver. +> If both sources are configured, offline mode will be selected. ### Configuration options @@ -73,17 +79,18 @@ Given below are the supported configurations: | Option name | Environment variable name | Type & Values | Default | Compatible resolver | |-----------------------|--------------------------------|------------------------|-----------|---------------------| -| host | FLAGD_HOST | String | localhost | RPC & in-process | -| port | FLAGD_PORT | int | 8013 | RPC & in-process | -| tls | FLAGD_TLS | boolean | false | RPC & in-process | -| socketPath | FLAGD_SOCKET_PATH | String | null | RPC & in-process | -| certPath | FLAGD_SERVER_CERT_PATH | String | null | RPC & in-process | -| deadline | FLAGD_DEADLINE_MS | int | 500 | RPC & in-process | +| host | FLAGD_HOST | String | localhost | rpc & in-process | +| port | FLAGD_PORT | int | 8013 | rpc & in-process | +| tls | FLAGD_TLS | boolean | false | rpc & in-process | +| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process | +| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process | +| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process | | selector | FLAGD_SOURCE_SELECTOR | String | null | in-process | -| cache | FLAGD_CACHE | String - lru, disabled | lru | RPC | -| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | RPC | -| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | RPC | -| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | RPC | +| cache | FLAGD_CACHE | String - lru, disabled | lru | rpc | +| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc | +| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc | +| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc | +| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | String | null | in-process | > [!NOTE] > Some configurations are only applicable for RPC resolver. diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java index 1422c5d6a..68564c51b 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java @@ -25,6 +25,7 @@ public final class Config { static final String BASE_EVENT_STREAM_RETRY_BACKOFF_MS_ENV_VAR_NAME = "FLAGD_RETRY_BACKOFF_MS"; static final String DEADLINE_MS_ENV_VAR_NAME = "FLAGD_DEADLINE_MS"; static final String SOURCE_SELECTOR_ENV_VAR_NAME = "FLAGD_SOURCE_SELECTOR"; + static final String OFFLINE_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH"; public static final String STATIC_REASON = "STATIC"; public static final String CACHED_REASON = "CACHED"; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java index 33bbe6c8d..32d2ae678 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java @@ -5,8 +5,6 @@ import lombok.Builder; import lombok.Getter; -import javax.annotation.Nonnull; - import static dev.openfeature.contrib.providers.flagd.Config.BASE_EVENT_STREAM_RETRY_BACKOFF_MS; import static dev.openfeature.contrib.providers.flagd.Config.BASE_EVENT_STREAM_RETRY_BACKOFF_MS_ENV_VAR_NAME; import static dev.openfeature.contrib.providers.flagd.Config.CACHE_ENV_VAR_NAME; @@ -22,6 +20,7 @@ import static dev.openfeature.contrib.providers.flagd.Config.HOST_ENV_VAR_NAME; import static dev.openfeature.contrib.providers.flagd.Config.MAX_CACHE_SIZE_ENV_VAR_NAME; import static dev.openfeature.contrib.providers.flagd.Config.MAX_EVENT_STREAM_RETRIES_ENV_VAR_NAME; +import static dev.openfeature.contrib.providers.flagd.Config.OFFLINE_SOURCE_PATH; import static dev.openfeature.contrib.providers.flagd.Config.PORT_ENV_VAR_NAME; import static dev.openfeature.contrib.providers.flagd.Config.SERVER_CERT_PATH_ENV_VAR_NAME; import static dev.openfeature.contrib.providers.flagd.Config.SOCKET_PATH_ENV_VAR_NAME; @@ -118,12 +117,8 @@ public class FlagdOptions { * File source of flags to be used by offline mode. * Setting this enables the offline mode of the in-process provider. */ - private String offlineFlagSourcePath; - - /** - * Flagd option to state the offline mode. Only get set with offlineFlagSourcePath. - */ - private boolean isOffline; + @Builder.Default + private String offlineFlagSourcePath = fallBackToEnvOrDefault(OFFLINE_SOURCE_PATH, null); /** * Inject OpenTelemetry for the library runtime. Providing sdk will initiate distributed tracing for flagd grpc @@ -135,31 +130,14 @@ public class FlagdOptions { * Overload default lombok builder. */ public static class FlagdOptionsBuilder { - - /** - * File source of flags to be used by offline mode. - * Setting this enables the offline mode of the in-process provider. - */ - public FlagdOptionsBuilder offlineFlagSourcePath(@Nonnull final String offlineFlagSourcePath) { - this.isOffline = true; - this.offlineFlagSourcePath = offlineFlagSourcePath; - - return this; - } - - // Remove the public access as this needs to be connected to offlineFlagSourcePath - @SuppressWarnings({"PMD.UnusedFormalParameter", "PMD.UnusedPrivateMethod"}) - private FlagdOptionsBuilder isOffline(final boolean isOffline) { - return this; - } - /** * Enable OpenTelemetry instance extraction from GlobalOpenTelemetry. Note that, this is only useful if global * configurations are registered. */ public FlagdOptionsBuilder withGlobalTelemetry(final boolean b) { - this.openTelemetry = GlobalOpenTelemetry.get(); - + if (b) { + this.openTelemetry = GlobalOpenTelemetry.get(); + } return this; } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 76a7367a4..51f0b55af 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -46,11 +46,7 @@ public class InProcessResolver implements Resolver { * Initialize an in-process resolver. */ public InProcessResolver(FlagdOptions options, Consumer stateConsumer) { - final Connector connector = options.isOffline() - ? new FileConnector(options.getOfflineFlagSourcePath()) - : new GrpcStreamConnector(options); - - this.flagStore = new FlagStore(connector); + this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); this.stateConsumer = stateConsumer; this.operator = new Operator(); @@ -153,6 +149,12 @@ public ProviderEvaluation objectEvaluation(String key, Value defaultValue .build(); } + static Connector getConnector(final FlagdOptions options) { + return options.getOfflineFlagSourcePath() != null && !options.getOfflineFlagSourcePath().isEmpty() + ? new FileConnector(options.getOfflineFlagSourcePath()) + : new GrpcStreamConnector(options); + } + private ProviderEvaluation resolve(Class type, String key, EvaluationContext ctx) { final FeatureFlag flag = flagStore.getFlag(key); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FeatureFlagProviderBuilderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java similarity index 95% rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FeatureFlagProviderBuilderTest.java rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java index 11ad26bf0..ca9a94afb 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FeatureFlagProviderBuilderTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdOptionsTest.java @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -public class FeatureFlagProviderBuilderTest { +public class FlagdOptionsTest { @Test public void TestDefaults() { @@ -31,7 +31,6 @@ public void TestDefaults() { assertNull(builder.getSelector()); assertNull(builder.getOpenTelemetry()); assertNull(builder.getOfflineFlagSourcePath()); - assertFalse(builder.isOffline()); } @Test @@ -59,7 +58,6 @@ public void TestBuilderOptions() { assertEquals(flagdOptions.getMaxCacheSize(), 100); assertEquals(flagdOptions.getMaxEventStreamRetries(), 1); assertEquals(flagdOptions.getSelector(), "app=weatherApp"); - assertTrue(flagdOptions.isOffline()); assertEquals("some-path", flagdOptions.getOfflineFlagSourcePath()); assertEquals(flagdOptions.getOpenTelemetry(), openTelemetry); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java index 9f1cdeccd..58800f238 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java @@ -45,7 +45,7 @@ public class StepDefinitions { * Tests run one at a time, but just in case, a lock is used to make sure the * client is not updated mid-test. * - * @param client client to inject into test. + * @param provider client to inject into test. */ public static void setProvider(FeatureProvider provider) { StepDefinitions.provider = provider; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 743672763..cdd2d1fce 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -1,8 +1,11 @@ package dev.openfeature.contrib.providers.flagd.resolver.process; +import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file.FileConnector; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.grpc.GrpcStreamConnector; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.MutableContext; @@ -35,12 +38,26 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.OBJECT_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.VARIANT_MISMATCH_FLAG; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; class InProcessResolverTest { + @Test + public void connectorSetup(){ + // given + FlagdOptions forGrpcOptions = + FlagdOptions.builder().resolverType(Config.Evaluator.IN_PROCESS).host("localhost").port(8080).build(); + FlagdOptions forOfflineOptions = + FlagdOptions.builder().resolverType(Config.Evaluator.IN_PROCESS).offlineFlagSourcePath("path").build(); + + // then + assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); + assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); + } + @Test public void eventHandling() throws Throwable { // given