From 440dfa2a5a59388a8ea0bc41216d7feb3da849c1 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 27 May 2026 12:27:45 -0400 Subject: [PATCH 1/6] Avoid blocking OpenFeature provider initialization --- .../trace/api/openfeature/DDEvaluator.java | 2 +- .../trace/api/openfeature/Provider.java | 9 +--- .../trace/api/openfeature/ProviderTest.java | 44 +++++++------------ 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index dcfe62db147..1bcd6cef814 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -58,7 +58,7 @@ public DDEvaluator(final Runnable configCallback) { public boolean initialize( final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception { FeatureFlaggingGateway.addConfigListener(this); - return initializationLatch.await(timeout, unit); // await for initialization + return initializationLatch.getCount() == 0; } @Override diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index f16b6e582a8..8f87fb8f6fd 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -13,7 +13,6 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.OpenFeatureError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import java.lang.reflect.Constructor; import java.util.Collections; import java.util.List; @@ -63,12 +62,8 @@ public Provider(final Options options) { public void initialize(final EvaluationContext context) throws Exception { try { evaluator = buildEvaluator(); - final boolean init = evaluator.initialize(options.getTimeout(), options.getUnit(), context); - initialized.set(init); - if (!init) { - throw new ProviderNotReadyError( - "Provider timed-out while waiting for initial configuration"); - } + initialized.set(true); + evaluator.initialize(options.getTimeout(), options.getUnit(), context); } catch (final OpenFeatureError e) { throw e; } catch (final Throwable e) { diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 87a80f59e20..a58a36d59f7 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -20,6 +20,7 @@ import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; import datadog.trace.api.openfeature.Provider.Options; import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventDetails; import dev.openfeature.sdk.Features; @@ -31,13 +32,9 @@ import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FatalError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -52,16 +49,8 @@ public class ProviderTest { @Captor private ArgumentCaptor eventDetailsCaptor; - private ExecutorService executor; - - @BeforeEach - public void setup() { - executor = Executors.newSingleThreadExecutor(); - } - @AfterEach public void tearDown() { - executor.shutdownNow(); OpenFeatureAPI.getInstance().shutdown(); FeatureFlaggingGateway.dispatch((ServerConfiguration) null); } @@ -72,7 +61,7 @@ public void testSetProvider() { api.setProvider(new Provider()); final Client client = api.getClient(); - assertThat(client.getProviderState(), equalTo(ProviderState.NOT_READY)); + await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); @@ -81,41 +70,40 @@ public void testSetProvider() { @Test public void testSetProviderAndWait() { final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - executor.submit(() -> api.setProviderAndWait(new Provider())); + api.setProviderAndWait(new Provider()); final Client client = api.getClient(); - assertThat(client.getProviderState(), equalTo(ProviderState.NOT_READY)); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); } @Test - public void testSetProviderAndWaitTimeout() { - final Consumer readyEvent = mock(Consumer.class); + public void testSetProviderAndWaitWithoutInitialConfiguration() { + final Consumer configChangedEvent = mock(Consumer.class); final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - client.on(ProviderEvent.PROVIDER_READY, readyEvent); + client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEvent); - // we time out after 10 millis without receiving the initial config - assertThrows( - ProviderNotReadyError.class, - () -> api.setProviderAndWait(new Provider(new Options().initTimeout(10, MILLISECONDS)))); + api.setProviderAndWait(new Provider(new Options().initTimeout(10, MILLISECONDS))); - // ready has not yet been called - verify(readyEvent, times(0)).accept(any()); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); + final FlagEvaluationDetails evalDetails = client.getStringDetails("missing", "default"); + assertThat(evalDetails.getValue(), equalTo("default")); + assertThat(evalDetails.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY)); // dispatch an initial configuration FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - // ready is called after receiving the configuration + // config changed is called after receiving the configuration await() .atMost(ofSeconds(1)) .untilAsserted( () -> { - verify(readyEvent, times(1)).accept(eventDetailsCaptor.capture()); - final EventDetails details = eventDetailsCaptor.getValue(); - assertThat(details.getProviderName(), equalTo(METADATA)); + verify(configChangedEvent, times(1)).accept(eventDetailsCaptor.capture()); + final EventDetails eventDetails = eventDetailsCaptor.getValue(); + assertThat(eventDetails.getProviderName(), equalTo(METADATA)); }); } From 291141a023596fa46fe91519333f41fa66c2a934 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 27 May 2026 12:40:59 -0400 Subject: [PATCH 2/6] Clarify OpenFeature provider config updates --- .../trace/api/openfeature/DDEvaluator.java | 8 ++------ .../datadog/trace/api/openfeature/Evaluator.java | 2 +- .../datadog/trace/api/openfeature/Provider.java | 15 +++------------ .../trace/api/openfeature/ProviderTest.java | 3 --- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index 1bcd6cef814..d87ea1cdd6e 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -35,7 +35,6 @@ import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; @@ -48,17 +47,15 @@ class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener { private final Runnable configCallback; private final AtomicReference configuration = new AtomicReference<>(); - private final CountDownLatch initializationLatch = new CountDownLatch(1); public DDEvaluator(final Runnable configCallback) { this.configCallback = configCallback; } @Override - public boolean initialize( - final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception { + public void initialize(final long timeout, final TimeUnit unit, final EvaluationContext context) + throws Exception { FeatureFlaggingGateway.addConfigListener(this); - return initializationLatch.getCount() == 0; } @Override @@ -69,7 +66,6 @@ public void shutdown() { @Override public void accept(final ServerConfiguration config) { configuration.set(config); - initializationLatch.countDown(); configCallback.run(); } diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java index 6ce3bac7b93..9a3998c4b03 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java @@ -6,7 +6,7 @@ interface Evaluator { - boolean initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception; + void initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception; void shutdown(); diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 8f87fb8f6fd..439a05229f4 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -17,7 +17,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,7 +28,6 @@ public class Provider extends EventProvider implements Metadata { private static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS); private volatile Evaluator evaluator; private final Options options; - private final AtomicBoolean initialized = new AtomicBoolean(false); private final FlagEvalMetrics flagEvalMetrics; private final FlagEvalHook flagEvalHook; @@ -62,7 +60,6 @@ public Provider(final Options options) { public void initialize(final EvaluationContext context) throws Exception { try { evaluator = buildEvaluator(); - initialized.set(true); evaluator.initialize(options.getTimeout(), options.getUnit(), context); } catch (final OpenFeatureError e) { throw e; @@ -72,15 +69,9 @@ public void initialize(final EvaluationContext context) throws Exception { } private void onConfigurationChange() { - if (initialized.getAndSet(true)) { - emit( - ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, - ProviderEventDetails.builder().message("New configuration received").build()); - } else { - emit( - ProviderEvent.PROVIDER_READY, - ProviderEventDetails.builder().message("Provider ready").build()); - } + emit( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, + ProviderEventDetails.builder().message("New configuration received").build()); } private Evaluator buildEvaluator() throws Exception { diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index a58a36d59f7..56b82fe3e27 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -9,7 +9,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -140,7 +139,6 @@ public void testGetProviderHooksReturnsFlagEvalHook() { @Test public void testShutdownCleansUpMetrics() throws Exception { Evaluator evaluator = mock(Evaluator.class); - when(evaluator.initialize(anyLong(), any(), any())).thenReturn(true); Provider provider = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); provider.initialize(null); provider.shutdown(); @@ -170,7 +168,6 @@ public void testProviderEvaluation( final String flag, final E defaultValue, final EvaluateMethod method) throws Exception { FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); final Evaluator evaluator = mock(Evaluator.class); - when(evaluator.initialize(anyLong(), any(), any())).thenReturn(true); when(evaluator.evaluate(any(), any(), any(), any())) .thenAnswer( invocation -> From 1c10ac81386c90238b830d1f0aa127a2be4b88e2 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 27 May 2026 13:18:29 -0400 Subject: [PATCH 3/6] Preserve OpenFeature evaluator initialization contract --- .../main/java/datadog/trace/api/openfeature/DDEvaluator.java | 5 +++-- .../main/java/datadog/trace/api/openfeature/Evaluator.java | 2 +- .../main/java/datadog/trace/api/openfeature/Provider.java | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index d87ea1cdd6e..55cd7c59f19 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -53,9 +53,10 @@ public DDEvaluator(final Runnable configCallback) { } @Override - public void initialize(final long timeout, final TimeUnit unit, final EvaluationContext context) - throws Exception { + public boolean initialize( + final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception { FeatureFlaggingGateway.addConfigListener(this); + return configuration.get() != null; } @Override diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java index 9a3998c4b03..6ce3bac7b93 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java @@ -6,7 +6,7 @@ interface Evaluator { - void initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception; + boolean initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception; void shutdown(); diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 439a05229f4..09e01fd0e80 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -60,7 +60,9 @@ public Provider(final Options options) { public void initialize(final EvaluationContext context) throws Exception { try { evaluator = buildEvaluator(); - evaluator.initialize(options.getTimeout(), options.getUnit(), context); + if (!evaluator.initialize(options.getTimeout(), options.getUnit(), context)) { + log.debug("OpenFeature provider initialized before initial remote configuration"); + } } catch (final OpenFeatureError e) { throw e; } catch (final Throwable e) { From af85d4b3fc908701322f4e31c12093cc07914ba0 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 27 May 2026 13:54:37 -0400 Subject: [PATCH 4/6] Restore blocking OpenFeature initialization recovery --- .../trace/api/openfeature/DDEvaluator.java | 15 +++++- .../trace/api/openfeature/Provider.java | 34 +++++++++++++- .../api/openfeature/DDEvaluatorTest.java | 28 +++++++++++ .../trace/api/openfeature/ProviderTest.java | 46 ++++++++++++------- 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index 55cd7c59f19..6c447d04634 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; @@ -47,6 +48,7 @@ class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener { private final Runnable configCallback; private final AtomicReference configuration = new AtomicReference<>(); + private final CountDownLatch initializationLatch = new CountDownLatch(1); public DDEvaluator(final Runnable configCallback) { this.configCallback = configCallback; @@ -56,7 +58,7 @@ public DDEvaluator(final Runnable configCallback) { public boolean initialize( final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception { FeatureFlaggingGateway.addConfigListener(this); - return configuration.get() != null; + return initializationLatch.await(timeout, unit); } @Override @@ -67,7 +69,16 @@ public void shutdown() { @Override public void accept(final ServerConfiguration config) { configuration.set(config); - configCallback.run(); + if (config != null) { + try { + configCallback.run(); + } finally { + // Let OpenFeature emit READY when blocking initialization returns successfully. + initializationLatch.countDown(); + } + } else if (initializationLatch.getCount() == 0) { + configCallback.run(); + } } @Override diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 09e01fd0e80..095ab4c010f 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -13,10 +13,12 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.OpenFeatureError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import java.lang.reflect.Constructor; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +30,8 @@ public class Provider extends EventProvider implements Metadata { private static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS); private volatile Evaluator evaluator; private final Options options; + private final AtomicReference initializationState = + new AtomicReference<>(InitializationState.NOT_STARTED); private final FlagEvalMetrics flagEvalMetrics; private final FlagEvalHook flagEvalHook; @@ -58,19 +62,40 @@ public Provider(final Options options) { @Override public void initialize(final EvaluationContext context) throws Exception { + initializationState.set(InitializationState.INITIALIZING); try { evaluator = buildEvaluator(); if (!evaluator.initialize(options.getTimeout(), options.getUnit(), context)) { - log.debug("OpenFeature provider initialized before initial remote configuration"); + initializationState.set(InitializationState.ERROR); + throw new ProviderNotReadyError( + "Provider timed-out while waiting for initial configuration"); } + initializationState.set(InitializationState.READY); } catch (final OpenFeatureError e) { + initializationState.set(InitializationState.ERROR); throw e; } catch (final Throwable e) { + initializationState.set(InitializationState.ERROR); throw new FatalError("Failed to initialize provider, is the tracer configured?", e); } } private void onConfigurationChange() { + final InitializationState state = initializationState.get(); + if (state == InitializationState.INITIALIZING) { + return; + } + if (state == InitializationState.ERROR + && initializationState.compareAndSet( + InitializationState.ERROR, InitializationState.READY)) { + emit( + ProviderEvent.PROVIDER_READY, + ProviderEventDetails.builder().message("Provider ready").build()); + return; + } + if (initializationState.get() != InitializationState.READY) { + return; + } emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().message("New configuration received").build()); @@ -148,6 +173,13 @@ protected Class loadEvaluatorClass() throws ClassNotFoundException { return Class.forName(EVALUATOR_IMPL); } + private enum InitializationState { + NOT_STARTED, + INITIALIZING, + READY, + ERROR + } + public static class Options { private long timeout; diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java index f13f8d2a2ba..a80eb10d13a 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java @@ -12,6 +12,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -79,6 +80,7 @@ public void setup() { @AfterEach public void tearDown() { FeatureFlaggingGateway.removeExposureListener(exposureListener); + FeatureFlaggingGateway.dispatch((ServerConfiguration) null); } private static Arguments[] valueMappingTestCases() { @@ -148,6 +150,32 @@ public void testEvaluateNoConfig() { assertThat(details.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY)); } + @Test + public void testInitializeTimesOutWithoutConfig() throws Exception { + final Runnable configCallback = mock(Runnable.class); + final DDEvaluator evaluator = new DDEvaluator(configCallback); + evaluator.accept(null); + try { + assertThat( + evaluator.initialize(10, MILLISECONDS, mock(EvaluationContext.class)), equalTo(false)); + verify(configCallback, times(0)).run(); + } finally { + evaluator.shutdown(); + } + } + + @Test + public void testInitializeWaitsForNonNullConfig() throws Exception { + FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); + final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class)); + try { + assertThat( + evaluator.initialize(10, MILLISECONDS, mock(EvaluationContext.class)), equalTo(true)); + } finally { + evaluator.shutdown(); + } + } + @Test public void testEvaluateNoContext() { final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class)); diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 56b82fe3e27..892a678003a 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -19,7 +19,6 @@ import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; import datadog.trace.api.openfeature.Provider.Options; import dev.openfeature.sdk.Client; -import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventDetails; import dev.openfeature.sdk.Features; @@ -31,9 +30,14 @@ import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -48,8 +52,16 @@ public class ProviderTest { @Captor private ArgumentCaptor eventDetailsCaptor; + private ExecutorService executor; + + @BeforeEach + public void setup() { + executor = Executors.newSingleThreadExecutor(); + } + @AfterEach public void tearDown() { + executor.shutdownNow(); OpenFeatureAPI.getInstance().shutdown(); FeatureFlaggingGateway.dispatch((ServerConfiguration) null); } @@ -60,47 +72,47 @@ public void testSetProvider() { api.setProvider(new Provider()); final Client client = api.getClient(); - await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); + assertThat(client.getProviderState(), equalTo(ProviderState.NOT_READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); } @Test - public void testSetProviderAndWait() { + public void testSetProviderAndWait() throws Exception { final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new Provider()); + final Future provider = executor.submit(() -> api.setProviderAndWait(new Provider())); final Client client = api.getClient(); - assertThat(client.getProviderState(), equalTo(ProviderState.READY)); + assertThat(client.getProviderState(), equalTo(ProviderState.NOT_READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); + provider.get(1, SECONDS); } @Test - public void testSetProviderAndWaitWithoutInitialConfiguration() { - final Consumer configChangedEvent = mock(Consumer.class); + public void testSetProviderAndWaitTimeoutRecoversWhenConfigurationArrives() { + final Consumer readyEvent = mock(Consumer.class); final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEvent); + client.on(ProviderEvent.PROVIDER_READY, readyEvent); - api.setProviderAndWait(new Provider(new Options().initTimeout(10, MILLISECONDS))); + assertThrows( + ProviderNotReadyError.class, + () -> api.setProviderAndWait(new Provider(new Options().initTimeout(10, MILLISECONDS)))); - assertThat(client.getProviderState(), equalTo(ProviderState.READY)); - final FlagEvaluationDetails evalDetails = client.getStringDetails("missing", "default"); - assertThat(evalDetails.getValue(), equalTo("default")); - assertThat(evalDetails.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY)); + assertThat(client.getProviderState(), equalTo(ProviderState.ERROR)); + verify(readyEvent, times(0)).accept(any()); - // dispatch an initial configuration FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - // config changed is called after receiving the configuration await() .atMost(ofSeconds(1)) .untilAsserted( () -> { - verify(configChangedEvent, times(1)).accept(eventDetailsCaptor.capture()); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); + verify(readyEvent, times(1)).accept(eventDetailsCaptor.capture()); final EventDetails eventDetails = eventDetailsCaptor.getValue(); assertThat(eventDetails.getProviderName(), equalTo(METADATA)); }); @@ -139,6 +151,7 @@ public void testGetProviderHooksReturnsFlagEvalHook() { @Test public void testShutdownCleansUpMetrics() throws Exception { Evaluator evaluator = mock(Evaluator.class); + when(evaluator.initialize(eq(10L), eq(MILLISECONDS), any())).thenReturn(true); Provider provider = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); provider.initialize(null); provider.shutdown(); @@ -168,6 +181,7 @@ public void testProviderEvaluation( final String flag, final E defaultValue, final EvaluateMethod method) throws Exception { FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); final Evaluator evaluator = mock(Evaluator.class); + when(evaluator.initialize(eq(10L), eq(SECONDS), any())).thenReturn(true); when(evaluator.evaluate(any(), any(), any(), any())) .thenAnswer( invocation -> From fcfa34c9cdf04af23f3a148bff1757ec5fe981e9 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 27 May 2026 21:45:45 -0400 Subject: [PATCH 5/6] Handle OpenFeature config recovery edge cases --- .../trace/api/openfeature/DDEvaluator.java | 15 +-- .../trace/api/openfeature/Evaluator.java | 2 + .../trace/api/openfeature/Provider.java | 45 ++++++++- .../api/openfeature/DDEvaluatorTest.java | 17 +++- .../trace/api/openfeature/ProviderTest.java | 91 +++++++++++++++++++ 5 files changed, 159 insertions(+), 11 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index 6c447d04634..91c0aafdc7a 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -58,7 +58,12 @@ public DDEvaluator(final Runnable configCallback) { public boolean initialize( final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception { FeatureFlaggingGateway.addConfigListener(this); - return initializationLatch.await(timeout, unit); + return initializationLatch.await(timeout, unit) || hasConfiguration(); + } + + @Override + public boolean hasConfiguration() { + return configuration.get() != null; } @Override @@ -70,12 +75,8 @@ public void shutdown() { public void accept(final ServerConfiguration config) { configuration.set(config); if (config != null) { - try { - configCallback.run(); - } finally { - // Let OpenFeature emit READY when blocking initialization returns successfully. - initializationLatch.countDown(); - } + initializationLatch.countDown(); + configCallback.run(); } else if (initializationLatch.getCount() == 0) { configCallback.run(); } diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java index 6ce3bac7b93..f4c9cacffdb 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Evaluator.java @@ -8,6 +8,8 @@ interface Evaluator { boolean initialize(long timeout, TimeUnit timeUnit, EvaluationContext context) throws Exception; + boolean hasConfiguration(); + void shutdown(); ProviderEvaluation evaluate( diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 095ab4c010f..83aa8a5f7fa 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -3,6 +3,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import de.thetaphi.forbiddenapis.SuppressForbidden; +import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.Hook; @@ -66,6 +67,21 @@ public void initialize(final EvaluationContext context) throws Exception { try { evaluator = buildEvaluator(); if (!evaluator.initialize(options.getTimeout(), options.getUnit(), context)) { + final InitializationState state = initializationState.get(); + if (state == InitializationState.READY + || initializationState.compareAndSet( + InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.READY)) { + return; + } + if (initializationState.compareAndSet( + InitializationState.INITIALIZING, InitializationState.ERROR)) { + throw new ProviderNotReadyError( + "Provider timed-out while waiting for initial configuration"); + } + if (initializationState.compareAndSet( + InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.READY)) { + return; + } initializationState.set(InitializationState.ERROR); throw new ProviderNotReadyError( "Provider timed-out while waiting for initial configuration"); @@ -80,9 +96,19 @@ public void initialize(final EvaluationContext context) throws Exception { } } - private void onConfigurationChange() { + void onConfigurationChange() { + if (evaluator == null || !evaluator.hasConfiguration()) { + onConfigurationUnavailable(); + return; + } + final InitializationState state = initializationState.get(); if (state == InitializationState.INITIALIZING) { + initializationState.compareAndSet( + InitializationState.INITIALIZING, InitializationState.INITIAL_CONFIG_RECEIVED); + return; + } + if (state == InitializationState.INITIAL_CONFIG_RECEIVED) { return; } if (state == InitializationState.ERROR @@ -101,6 +127,22 @@ private void onConfigurationChange() { ProviderEventDetails.builder().message("New configuration received").build()); } + private void onConfigurationUnavailable() { + final InitializationState state = initializationState.get(); + if (state != InitializationState.READY) { + return; + } + if (!initializationState.compareAndSet(InitializationState.READY, InitializationState.ERROR)) { + return; + } + emit( + ProviderEvent.PROVIDER_ERROR, + ProviderEventDetails.builder() + .message("Configuration unavailable") + .errorCode(ErrorCode.PROVIDER_NOT_READY) + .build()); + } + private Evaluator buildEvaluator() throws Exception { if (evaluator != null) { return evaluator; @@ -176,6 +218,7 @@ protected Class loadEvaluatorClass() throws ClassNotFoundException { private enum InitializationState { NOT_STARTED, INITIALIZING, + INITIAL_CONFIG_RECEIVED, READY, ERROR } diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java index a80eb10d13a..bb86c409bad 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java @@ -13,6 +13,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -53,6 +54,9 @@ import java.util.List; import java.util.Map; import java.util.TimeZone; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -166,12 +170,19 @@ public void testInitializeTimesOutWithoutConfig() throws Exception { @Test public void testInitializeWaitsForNonNullConfig() throws Exception { - FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class)); + final ExecutorService executor = Executors.newSingleThreadExecutor(); try { - assertThat( - evaluator.initialize(10, MILLISECONDS, mock(EvaluationContext.class)), equalTo(true)); + final Future initialized = + executor.submit(() -> evaluator.initialize(1, SECONDS, mock(EvaluationContext.class))); + + evaluator.accept(null); + assertThat(initialized.isDone(), equalTo(false)); + + evaluator.accept(mock(ServerConfiguration.class)); + assertThat(initialized.get(1, SECONDS), equalTo(true)); } finally { + executor.shutdownNow(); evaluator.shutdown(); } } diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 892a678003a..77dfb305a28 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -19,6 +19,7 @@ import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; import datadog.trace.api.openfeature.Provider.Options; import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventDetails; import dev.openfeature.sdk.Features; @@ -118,6 +119,96 @@ public void testSetProviderAndWaitTimeoutRecoversWhenConfigurationArrives() { }); } + @Test + public void testSetProviderAndWaitCompletesWhenConfigurationArrivesAtTimeoutBoundary() + throws Exception { + final Provider[] providerRef = new Provider[1]; + final Evaluator evaluator = + new Evaluator() { + private boolean hasConfiguration; + + @Override + public boolean initialize( + final long timeout, + final java.util.concurrent.TimeUnit timeUnit, + final EvaluationContext context) { + hasConfiguration = true; + providerRef[0].onConfigurationChange(); + return false; + } + + @Override + public boolean hasConfiguration() { + return hasConfiguration; + } + + @Override + public void shutdown() {} + + @Override + public ProviderEvaluation evaluate( + final Class target, + final String key, + final T defaultValue, + final EvaluationContext context) { + return ProviderEvaluation.builder().value(defaultValue).build(); + } + }; + + final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + providerRef[0] = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); + api.setProviderAndWait(providerRef[0]); + + final Client client = api.getClient(); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); + } + + @Test + public void testNullConfigurationAfterReadyTransitionsToErrorAndRecovers() { + final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProvider(new Provider()); + final Client client = api.getClient(); + + FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); + await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); + + final Consumer errorEvent = mock(Consumer.class); + final Consumer readyEvent = mock(Consumer.class); + final Consumer configChangedEvent = mock(Consumer.class); + client.on(ProviderEvent.PROVIDER_ERROR, errorEvent); + client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEvent); + + FeatureFlaggingGateway.dispatch((ServerConfiguration) null); + await() + .atMost(ofSeconds(1)) + .untilAsserted( + () -> { + assertThat(client.getProviderState(), equalTo(ProviderState.ERROR)); + verify(errorEvent, times(1)).accept(eventDetailsCaptor.capture()); + final EventDetails eventDetails = eventDetailsCaptor.getValue(); + assertThat(eventDetails.getProviderName(), equalTo(METADATA)); + }); + + final FlagEvaluationDetails evalDetails = client.getStringDetails("missing", "default"); + assertThat(evalDetails.getValue(), equalTo("default")); + assertThat(evalDetails.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY)); + + client.on(ProviderEvent.PROVIDER_READY, readyEvent); + FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); + await() + .atMost(ofSeconds(1)) + .untilAsserted( + () -> { + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); + verify(readyEvent, times(1)).accept(any()); + }); + + FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); + await() + .atMost(ofSeconds(1)) + .untilAsserted(() -> verify(configChangedEvent, times(1)).accept(any())); + } + @Test public void testFailureToLoadInternalApi() { @SuppressWarnings("unchecked") From 772f4aeb6af34d005b6f5b95271c1ff8e6759273 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 21:23:02 -0400 Subject: [PATCH 6/6] Handle OpenFeature initialization race windows --- .../trace/api/openfeature/Provider.java | 52 +++++---- .../trace/api/openfeature/ProviderTest.java | 104 ++++++++++++++++++ 2 files changed, 137 insertions(+), 19 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 83aa8a5f7fa..c492ef49c69 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -67,31 +67,23 @@ public void initialize(final EvaluationContext context) throws Exception { try { evaluator = buildEvaluator(); if (!evaluator.initialize(options.getTimeout(), options.getUnit(), context)) { - final InitializationState state = initializationState.get(); - if (state == InitializationState.READY - || initializationState.compareAndSet( - InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.READY)) { + if (markInitialConfigReceivedReady()) { return; } - if (initializationState.compareAndSet( - InitializationState.INITIALIZING, InitializationState.ERROR)) { - throw new ProviderNotReadyError( - "Provider timed-out while waiting for initial configuration"); - } - if (initializationState.compareAndSet( - InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.READY)) { - return; - } - initializationState.set(InitializationState.ERROR); + markInitializationError(); + throw new ProviderNotReadyError( + "Provider timed-out while waiting for initial configuration"); + } + if (!evaluator.hasConfiguration() || !markSuccessfulInitializationReady()) { + markInitializationError(); throw new ProviderNotReadyError( "Provider timed-out while waiting for initial configuration"); } - initializationState.set(InitializationState.READY); } catch (final OpenFeatureError e) { - initializationState.set(InitializationState.ERROR); + markInitializationError(); throw e; } catch (final Throwable e) { - initializationState.set(InitializationState.ERROR); + markInitializationError(); throw new FatalError("Failed to initialize provider, is the tracer configured?", e); } } @@ -128,8 +120,8 @@ void onConfigurationChange() { } private void onConfigurationUnavailable() { - final InitializationState state = initializationState.get(); - if (state != InitializationState.READY) { + if (initializationState.compareAndSet( + InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.ERROR)) { return; } if (!initializationState.compareAndSet(InitializationState.READY, InitializationState.ERROR)) { @@ -143,6 +135,28 @@ private void onConfigurationUnavailable() { .build()); } + private boolean markInitialConfigReceivedReady() { + return initializationState.get() == InitializationState.READY + || initializationState.compareAndSet( + InitializationState.INITIAL_CONFIG_RECEIVED, InitializationState.READY); + } + + private boolean markSuccessfulInitializationReady() { + return markInitialConfigReceivedReady() + || initializationState.compareAndSet( + InitializationState.INITIALIZING, InitializationState.READY); + } + + private void markInitializationError() { + InitializationState state = initializationState.get(); + while (state != InitializationState.READY && state != InitializationState.ERROR) { + if (initializationState.compareAndSet(state, InitializationState.ERROR)) { + return; + } + state = initializationState.get(); + } + } + private Evaluator buildEvaluator() throws Exception { if (evaluator != null) { return evaluator; diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 77dfb305a28..27d4dd5d2b5 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -32,10 +32,12 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import java.lang.reflect.Field; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -163,6 +165,99 @@ public ProviderEvaluation evaluate( assertThat(client.getProviderState(), equalTo(ProviderState.READY)); } + @Test + public void testSetProviderAndWaitFailsWhenConfigurationIsRemovedBeforeInitializationCompletes() { + final Provider[] providerRef = new Provider[1]; + final Evaluator evaluator = + new Evaluator() { + private boolean hasConfiguration; + + @Override + public boolean initialize( + final long timeout, + final java.util.concurrent.TimeUnit timeUnit, + final EvaluationContext context) { + hasConfiguration = true; + providerRef[0].onConfigurationChange(); + hasConfiguration = false; + providerRef[0].onConfigurationChange(); + return true; + } + + @Override + public boolean hasConfiguration() { + return hasConfiguration; + } + + @Override + public void shutdown() {} + + @Override + public ProviderEvaluation evaluate( + final Class target, + final String key, + final T defaultValue, + final EvaluationContext context) { + return ProviderEvaluation.builder().value(defaultValue).build(); + } + }; + + final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + providerRef[0] = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); + + assertThrows(ProviderNotReadyError.class, () -> api.setProviderAndWait(providerRef[0])); + + final Client client = api.getClient(); + assertThat(client.getProviderState(), equalTo(ProviderState.ERROR)); + } + + @Test + public void testInitializationErrorDoesNotOverwriteRecoveredReadyState() throws Exception { + final Provider[] providerRef = new Provider[1]; + final Evaluator evaluator = + new Evaluator() { + private boolean hasConfiguration; + + @Override + public boolean initialize( + final long timeout, + final java.util.concurrent.TimeUnit timeUnit, + final EvaluationContext context) { + hasConfiguration = true; + providerRef[0].onConfigurationChange(); + hasConfiguration = false; + providerRef[0].onConfigurationChange(); + hasConfiguration = true; + providerRef[0].onConfigurationChange(); + throw new ProviderNotReadyError( + "Provider timed-out while waiting for initial configuration"); + } + + @Override + public boolean hasConfiguration() { + return hasConfiguration; + } + + @Override + public void shutdown() {} + + @Override + public ProviderEvaluation evaluate( + final Class target, + final String key, + final T defaultValue, + final EvaluationContext context) { + return ProviderEvaluation.builder().value(defaultValue).build(); + } + }; + + providerRef[0] = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); + + assertThrows(ProviderNotReadyError.class, () -> providerRef[0].initialize(null)); + + assertThat(initializationState(providerRef[0]), equalTo("READY")); + } + @Test public void testNullConfigurationAfterReadyTransitionsToErrorAndRecovers() { final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); @@ -243,6 +338,7 @@ public void testGetProviderHooksReturnsFlagEvalHook() { public void testShutdownCleansUpMetrics() throws Exception { Evaluator evaluator = mock(Evaluator.class); when(evaluator.initialize(eq(10L), eq(MILLISECONDS), any())).thenReturn(true); + when(evaluator.hasConfiguration()).thenReturn(true); Provider provider = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); provider.initialize(null); provider.shutdown(); @@ -273,6 +369,7 @@ public void testProviderEvaluation( FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); final Evaluator evaluator = mock(Evaluator.class); when(evaluator.initialize(eq(10L), eq(SECONDS), any())).thenReturn(true); + when(evaluator.hasConfiguration()).thenReturn(true); when(evaluator.evaluate(any(), any(), any(), any())) .thenAnswer( invocation -> @@ -290,4 +387,11 @@ public void testProviderEvaluation( verify(evaluator, times(1)) .evaluate(any(), eq(flag), eq(defaultValue), any(EvaluationContext.class)); } + + private static String initializationState(final Provider provider) throws Exception { + final Field stateField = Provider.class.getDeclaredField("initializationState"); + stateField.setAccessible(true); + final AtomicReference state = (AtomicReference) stateField.get(provider); + return state.get().toString(); + } }