diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java
index b54d895..eadbdc2 100644
--- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java
+++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java
@@ -51,6 +51,8 @@ public String getName() {
private final Object stateLock = new Object();
+ private boolean initializing = false;
+
/**
* Create a provider with the specified SDK and default configuration.
*
@@ -152,6 +154,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
setState(ProviderState.READY);
}
+ setInitializing(true);
var completer = new CompletableFuture();
client.getFlagTracker().addFlagChangeListener(detail -> {
@@ -164,11 +167,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
});
if (getState() == ProviderState.READY) {
+ setInitializing(false);
return;
}
- handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
- var successfullyInitialized = completer.get();
+ boolean successfullyInitialized;
+ try {
+ handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
+ successfullyInitialized = completer.get();
+ } finally {
+ setInitializing(false);
+ }
if(!successfullyInitialized) {
throw new RuntimeException("Failed to initialize LaunchDarkly client.");
@@ -189,19 +198,24 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet
}
break;
case VALID: {
+ boolean becameReady = false;
boolean emit = false;
synchronized (stateLock) {
// If we are ready, then we don't want to emit it again. Other conditions we may be updating the
// reason we are stale or interrupted, so we want to emit an event each time.
if (state != ProviderState.READY) {
- emit = true;
- setState(ProviderState.READY);
+ becameReady = true;
+ // The OpenFeature SDK emits its own ready event when initialization succeeds.
+ emit = !initializing;
+ state = ProviderState.READY;
}
}
- if (emit) {
+ if (becameReady) {
completer.complete(true);
- emitProviderReady(ProviderEventDetails.builder().build());
+ if (emit) {
+ emitProviderReady(ProviderEventDetails.builder().build());
+ }
}
}
break;
@@ -218,6 +232,12 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet
}
}
+ private void setInitializing(boolean initializing) {
+ synchronized (stateLock) {
+ this.initializing = initializing;
+ }
+ }
+
private void setState(ProviderState state) {
synchronized (stateLock) {
this.state = state;
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java
index fafd372..929b204 100644
--- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java
+++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java
@@ -104,6 +104,29 @@ public void close() throws IOException {
}
}
+class ControllableDataSource implements DataSource {
+ public Future start() {
+ return new CompletableFuture<>();
+ }
+
+ public boolean isInitialized() {
+ return false;
+ }
+
+ public void close() throws IOException {
+ }
+}
+
+class ControllableDataSourceFactory implements ComponentConfigurer {
+ final CompletableFuture sink = new CompletableFuture<>();
+
+ @Override
+ public DataSource build(ClientContext clientContext) {
+ sink.complete(clientContext.getDataSourceUpdateSink());
+ return new ControllableDataSource();
+ }
+}
+
class DelayedDataSourceFactory implements ComponentConfigurer {
private Duration startDelay;
private boolean willError;
@@ -219,14 +242,19 @@ public void itCanHandleClientThatIsNotInitializedImmediately() throws Exception
assertEquals(ProviderState.NOT_READY, provider.getState());
var readyCount = new AtomicInteger();
+ CompletableFuture gotReadyEvent = new CompletableFuture<>();
OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
readyCount.getAndIncrement();
+ gotReadyEvent.complete(true);
});
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
assertEquals(ProviderState.READY, provider.getState());
+ assertTrue(gotReadyEvent.get(1000, TimeUnit.MILLISECONDS));
+
+ Thread.sleep(100);
assertEquals(1, readyCount.get());
}
@@ -260,6 +288,54 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E
assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS));
}
+ @Test
+ public void itEmitsReadyWhenTheDataSourceRecoversFromAFailedInitialization() throws Exception {
+ var dataSourceFactory = new ControllableDataSourceFactory();
+ var config = new LDConfig.Builder()
+ .startWait(Duration.ZERO)
+ .dataSource(dataSourceFactory)
+ .events(Components.noEvents())
+ .build();
+ var provider = new Provider("fake-key", config);
+ var sink = dataSourceFactory.sink.get(1000, TimeUnit.MILLISECONDS);
+
+ var readyCount = new AtomicInteger();
+ CompletableFuture gotReadyEvent = new CompletableFuture<>();
+ CompletableFuture gotErrorEvent = new CompletableFuture<>();
+
+ OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
+ readyCount.getAndIncrement();
+ gotReadyEvent.complete(true);
+ });
+
+ OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> {
+ gotErrorEvent.complete(true);
+ });
+
+ sink.updateStatus(DataSourceStatusProvider.State.OFF, new DataSourceStatusProvider.ErrorInfo(
+ DataSourceStatusProvider.ErrorKind.NETWORK_ERROR,
+ 404,
+ "bad",
+ LocalDateTime.now().toInstant(ZoneOffset.UTC)));
+
+ GeneralError initializationError = null;
+ try {
+ OpenFeatureAPI.getInstance().setProviderAndWait(provider);
+ } catch (GeneralError e) {
+ initializationError = e;
+ }
+
+ assertNotNull(initializationError);
+ assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS));
+
+ sink.updateStatus(DataSourceStatusProvider.State.VALID, null);
+
+ assertTrue(gotReadyEvent.get(1000, TimeUnit.MILLISECONDS));
+
+ Thread.sleep(100);
+ assertEquals(1, readyCount.get());
+ }
+
@Test
public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception {
var config = new LDConfig.Builder()