Skip to content

Commit 0576d74

Browse files
docs: Mark initialization as supported in the feature matrix
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
2 parents a40a6d7 + 373eac5 commit 0576d74

3 files changed

Lines changed: 130 additions & 11 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ This provider is designed primarily for use in multi-user systems such as web se
1414

1515
This version of the LaunchDarkly provider works with Java 11 and above.
1616

17+
## Feature matrix
18+
19+
This matrix mirrors the [feature matrix of the OpenFeature SDK for Java](https://github.com/open-feature/java-sdk#-features) and describes what this provider supports. Rows which are not supported state whether the limitation comes from the OpenFeature Java SDK or from the provider.
20+
21+
| Status | Feature | Notes |
22+
|--------|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
23+
|| Providers | Evaluates boolean, string, integer, double, and object flags through the LaunchDarkly Java SDK. |
24+
|| Targeting | The `EvaluationContext` is converted to a LaunchDarkly single or multi-context. See [OpenFeature Specific Considerations](#openfeature-specific-considerations). |
25+
|| Multi-provider (experimental) | Provided by the OpenFeature SDK, which delegates to each provider in turn; no provider support is required. |
26+
|| Hooks | Hooks are registered on the OpenFeature API and client; the provider requires no additional support and its results are visible to hooks, including [flag metadata](#flag-metadata). |
27+
|| Tracking | `track` sends a LaunchDarkly custom event for the evaluation context, with the tracking event value and remaining details attached. |
28+
|| Logging | The provider logs through the logging configuration of the `LDConfig` it is given. |
29+
|| Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
30+
|| Eventing | LaunchDarkly data source status changes are emitted as `PROVIDER_READY`, `PROVIDER_STALE`, and `PROVIDER_ERROR`. Flag changes are emitted as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. |
31+
|| Initialization | `initialize` reports whether the LaunchDarkly client became ready, and a failure results in the `ERROR` state so that cached or fallback flag data is still evaluated. `Provider(String, LDConfig, Duration)` bounds initialization with a start wait duration; the other constructors wait until the data source becomes valid or permanently fails. |
32+
|| Shutdown | `shutdown` closes the LaunchDarkly client. A closed client cannot be restarted, so a new provider instance is required afterward. |
33+
|| Transaction Context Propagation | Provided by the OpenFeature SDK, which merges the transaction context into the evaluation context before the provider is called; no provider support is required. |
34+
|| Extending | This provider is itself an extension of the OpenFeature SDK. The underlying LaunchDarkly client is available through `getLdClient()` for functionality with no OpenFeature equivalent. |
35+
|| Flag metadata | LaunchDarkly evaluation reason details are returned as OpenFeature flag metadata. See [Flag Metadata](#flag-metadata). |
36+
37+
<sub>Supported: ✅ | Partially supported: ⚠️ | Not supported: ❌</sub>
38+
1739
## Getting started
1840

1941
### Requisites

src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public String getName() {
5353

5454
private final Object stateLock = new Object();
5555

56+
private boolean initializing = false;
57+
5658
/**
5759
* Create a provider with the specified SDK and default configuration.
5860
* <p>
@@ -182,6 +184,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
182184
setState(ProviderState.READY);
183185
}
184186

187+
setInitializing(true);
185188
var completer = new CompletableFuture<Boolean>();
186189

187190
client.getFlagTracker().addFlagChangeListener(detail -> {
@@ -194,19 +197,26 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
194197
});
195198

196199
if (getState() == ProviderState.READY) {
200+
setInitializing(false);
197201
return;
198202
}
199203

200-
handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
204+
boolean successfullyInitialized;
205+
try {
206+
handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
201207

202-
// With a start wait the client constructor has already waited, so the data source has either become valid,
203-
// failed permanently, or run out of time; the outcome is whatever it is now.
204-
if (!startWait.isZero() && !completer.isDone()) {
205-
setState(ProviderState.ERROR);
206-
throw new RuntimeException("The client did not initialize within the start wait duration.");
208+
// With a start wait the client constructor has already waited, so the data source has either become valid,
209+
// failed permanently, or run out of time; the outcome is whatever it is now.
210+
if (!startWait.isZero() && !completer.isDone()) {
211+
setState(ProviderState.ERROR);
212+
throw new RuntimeException("The client did not initialize within the start wait duration.");
213+
}
214+
successfullyInitialized = completer.get();
215+
} finally {
216+
setInitializing(false);
207217
}
208218

209-
if (!completer.get()) {
219+
if (!successfullyInitialized) {
210220
throw new RuntimeException("Failed to initialize LaunchDarkly client.");
211221
}
212222
}
@@ -225,19 +235,24 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet
225235
}
226236
break;
227237
case VALID: {
238+
boolean becameReady = false;
228239
boolean emit = false;
229240
synchronized (stateLock) {
230241
// If we are ready, then we don't want to emit it again. Other conditions we may be updating the
231242
// reason we are stale or interrupted, so we want to emit an event each time.
232243
if (state != ProviderState.READY) {
233-
emit = true;
234-
setState(ProviderState.READY);
244+
becameReady = true;
245+
// The OpenFeature SDK emits its own ready event when initialization succeeds.
246+
emit = !initializing;
247+
state = ProviderState.READY;
235248
}
236249
}
237250

238-
if (emit) {
251+
if (becameReady) {
239252
completer.complete(true);
240-
emitProviderReady(ProviderEventDetails.builder().build());
253+
if (emit) {
254+
emitProviderReady(ProviderEventDetails.builder().build());
255+
}
241256
}
242257
}
243258
break;
@@ -254,6 +269,12 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet
254269
}
255270
}
256271

272+
private void setInitializing(boolean initializing) {
273+
synchronized (stateLock) {
274+
this.initializing = initializing;
275+
}
276+
}
277+
257278
private void setState(ProviderState state) {
258279
synchronized (stateLock) {
259280
this.state = state;

src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,29 @@ public void close() throws IOException {
106106
}
107107
}
108108

109+
class ControllableDataSource implements DataSource {
110+
public Future<Void> start() {
111+
return new CompletableFuture<>();
112+
}
113+
114+
public boolean isInitialized() {
115+
return false;
116+
}
117+
118+
public void close() throws IOException {
119+
}
120+
}
121+
122+
class ControllableDataSourceFactory implements ComponentConfigurer<DataSource> {
123+
final CompletableFuture<DataSourceUpdateSink> sink = new CompletableFuture<>();
124+
125+
@Override
126+
public DataSource build(ClientContext clientContext) {
127+
sink.complete(clientContext.getDataSourceUpdateSink());
128+
return new ControllableDataSource();
129+
}
130+
}
131+
109132
class DelayedDataSourceFactory implements ComponentConfigurer<DataSource> {
110133
private Duration startDelay;
111134
private boolean willError;
@@ -254,14 +277,19 @@ public void itCanHandleClientThatIsNotInitializedImmediately() throws Exception
254277
assertEquals(ProviderState.NOT_READY, provider.getState());
255278

256279
var readyCount = new AtomicInteger();
280+
CompletableFuture<Boolean> gotReadyEvent = new CompletableFuture<>();
257281

258282
OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
259283
readyCount.getAndIncrement();
284+
gotReadyEvent.complete(true);
260285
});
261286

262287
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
263288

264289
assertEquals(ProviderState.READY, provider.getState());
290+
assertTrue(gotReadyEvent.get(1000, TimeUnit.MILLISECONDS));
291+
292+
Thread.sleep(100);
265293
assertEquals(1, readyCount.get());
266294
}
267295

@@ -366,6 +394,54 @@ public void initializationWaitsIndefinitelyWhenStartWaitIsZero() throws Exceptio
366394
provider.shutdown();
367395
}
368396

397+
@Test
398+
public void itEmitsReadyWhenTheDataSourceRecoversFromAFailedInitialization() throws Exception {
399+
var dataSourceFactory = new ControllableDataSourceFactory();
400+
var config = new LDConfig.Builder()
401+
.startWait(Duration.ZERO)
402+
.dataSource(dataSourceFactory)
403+
.events(Components.noEvents())
404+
.build();
405+
var provider = new Provider("fake-key", config);
406+
var sink = dataSourceFactory.sink.get(1000, TimeUnit.MILLISECONDS);
407+
408+
var readyCount = new AtomicInteger();
409+
CompletableFuture<Boolean> gotReadyEvent = new CompletableFuture<>();
410+
CompletableFuture<Boolean> gotErrorEvent = new CompletableFuture<>();
411+
412+
OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
413+
readyCount.getAndIncrement();
414+
gotReadyEvent.complete(true);
415+
});
416+
417+
OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> {
418+
gotErrorEvent.complete(true);
419+
});
420+
421+
sink.updateStatus(DataSourceStatusProvider.State.OFF, new DataSourceStatusProvider.ErrorInfo(
422+
DataSourceStatusProvider.ErrorKind.NETWORK_ERROR,
423+
404,
424+
"bad",
425+
LocalDateTime.now().toInstant(ZoneOffset.UTC)));
426+
427+
GeneralError initializationError = null;
428+
try {
429+
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
430+
} catch (GeneralError e) {
431+
initializationError = e;
432+
}
433+
434+
assertNotNull(initializationError);
435+
assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS));
436+
437+
sink.updateStatus(DataSourceStatusProvider.State.VALID, null);
438+
439+
assertTrue(gotReadyEvent.get(1000, TimeUnit.MILLISECONDS));
440+
441+
Thread.sleep(100);
442+
assertEquals(1, readyCount.get());
443+
}
444+
369445
@Test
370446
public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception {
371447
var config = new LDConfig.Builder()

0 commit comments

Comments
 (0)