Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Java](https:/
| ✅ | Logging | The provider logs through the logging configuration of the `LDConfig` it is given. |
| ✅ | Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
| ✅ | 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. |
| ⚠️ | 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. It has no timeout of its own and waits until the data source becomes valid or permanently fails: [#58](https://github.com/launchdarkly/openfeature-java-server/issues/58). |
| | Initialization | `initialize` reports whether the LaunchDarkly client became ready within the start wait time, and a failure results in the `ERROR` state so that cached or fallback flag data is still evaluated. The wait is bounded by `LDConfig.Builder.startWait` or by the start wait time given to the provider constructor, which may be null to wait indefinitely. |
| ✅ | Shutdown | `shutdown` closes the LaunchDarkly client. A closed client cannot be restarted, so a new provider instance is required afterward. |
| ✅ | 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. |
| ✅ | 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. |
Expand Down Expand Up @@ -107,7 +107,13 @@ There are several other attributes which have special functionality within a sin

### Initialization and Shutdown

The LaunchDarkly supports Initialization and Shutdown using the OpenFeature API. The provider begins initialization as soon as it is constructed, and the underlying LaunchDarkly SDK will block execution based on the configured start wait time. If you wish to defer the blocking behavior, then you can use the `startWait` function when building the `LDConfig`.
The LaunchDarkly supports Initialization and Shutdown using the OpenFeature API. The provider begins initialization as soon as it is constructed, and the underlying LaunchDarkly SDK will block execution based on the configured start wait time. If you wish to defer the blocking behavior, then you can use the `startWait` function when building the `LDConfig`, or pass a start wait time to the provider constructor.

The start wait time bounds the whole of initialization. `Duration.ZERO` waits nowhere, so initialization fails and the application learns when the provider becomes usable from provider events. A null start wait time waits indefinitely, without blocking during construction, until the data source becomes valid or fails permanently.

```java
var provider = new Provider(sdkKey, config, null);
```

OpenFeature will report when the provider is ready, and additionally the `setProviderAndWait` function of the OpenFeature
API can be used to wait until the provider is ready, or it has encountered a permanent error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import dev.openfeature.sdk.*;

import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -53,6 +54,8 @@ public String getName() {

private boolean initializing = false;

private final boolean waitIndefinitely;

/**
* Create a provider with the specified SDK and default configuration.
* <p>
Expand All @@ -71,14 +74,42 @@ public Provider(String sdkKey) {
* @param config a client configuration object
*/
public Provider(String sdkKey, LDConfig config) {
this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config)
this(makeClient(sdkKey, LDConfig.Builder.fromConfig(config)), false);
}

/**
* Create a provider with the specified SDK key, configuration and start wait time.
* <p>
* The start wait time replaces the one configured with {@link LDConfig.Builder#startWait(Duration)} and bounds the
* whole of initialization: the LaunchDarkly client waits for up to that long while it is constructed, and
* {@link Provider#initialize(EvaluationContext)} then reports whether it became ready. {@link Duration#ZERO} waits
* nowhere, so initialization fails unless the client is already ready and the application learns when it becomes
* usable from provider events. A null start wait time waits indefinitely: nothing is waited for during
* construction, and initialization does not complete until the data source becomes valid or fails permanently.
*
* @param sdkKey the SDK key for your LaunchDarkly environment
* @param config a client configuration object
* @param startWait how long to wait for the client to become ready, or null to wait indefinitely
*/
public Provider(String sdkKey, LDConfig config, Duration startWait) {
this(makeClient(sdkKey, LDConfig.Builder.fromConfig(config)
.startWait(startWait == null ? Duration.ZERO : startWait)), startWait == null);
}

private static LDClient makeClient(String sdkKey, LDConfig.Builder builder) {
return new LDClient(sdkKey, builder
.wrapper(Components.wrapperInfo()
.wrapperName("open-feature-java-server")
.wrapperVersion(Version.SDK_VERSION)).build()));
.wrapperVersion(Version.SDK_VERSION)).build());
}

Provider(LDClientInterface client) {
this(client, true);
}

Provider(LDClientInterface client, boolean waitIndefinitely) {
this.client = client;
this.waitIndefinitely = waitIndefinitely;
logger = client.getLogger();
evaluationContextConverter = new EvaluationContextConverter(logger);
evaluationDetailConverter = new EvaluationDetailConverter(logger);
Expand Down Expand Up @@ -174,6 +205,9 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
boolean successfullyInitialized;
try {
handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
if (!waitIndefinitely) {
completer.complete(false);
}
successfullyInitialized = completer.get();
} finally {
setInitializing(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class DelayedDataSource implements DataSource {
Expand Down Expand Up @@ -238,7 +239,7 @@ public void itCanHandleClientThatIsNotInitializedImmediately() throws Exception
.dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false))
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config);
var provider = new Provider("fake-key", config, null);
assertEquals(ProviderState.NOT_READY, provider.getState());

var readyCount = new AtomicInteger();
Expand All @@ -265,7 +266,7 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E
.dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), true))
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config);
var provider = new Provider("fake-key", config, null);
assertEquals(ProviderState.NOT_READY, provider.getState());

CompletableFuture<Boolean> gotErrorEvent = new CompletableFuture<>();
Expand Down Expand Up @@ -296,7 +297,7 @@ public void itEmitsReadyWhenTheDataSourceRecoversFromAFailedInitialization() thr
.dataSource(dataSourceFactory)
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config);
var provider = new Provider("fake-key", config, null);
var sink = dataSourceFactory.sink.get(1000, TimeUnit.MILLISECONDS);

var readyCount = new AtomicInteger();
Expand Down Expand Up @@ -343,7 +344,7 @@ public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception {
.dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false, true))
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config);
var provider = new Provider("fake-key", config, null);
CompletableFuture<String> errorMessage = new CompletableFuture<>();

OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> {
Expand All @@ -364,7 +365,7 @@ public void itIncludesHttpDataSourceErrorInErrorEvents() throws Exception {
.dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false, true, true))
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config);
var provider = new Provider("fake-key", config, null);
CompletableFuture<String> errorMessage = new CompletableFuture<>();

OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> {
Expand All @@ -378,4 +379,33 @@ public void itIncludesHttpDataSourceErrorInErrorEvents() throws Exception {
assertTrue(!message.isEmpty());
assertTrue(message.contains("401"));
}

@Test
public void itDoesNotWaitAgainWithAStartWaitTime() {
var config = new LDConfig.Builder()
.dataSource(new DelayedDataSourceFactory(Duration.ofSeconds(30), false))
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config, Duration.ofMillis(50));

var started = System.currentTimeMillis();
assertThrows(GeneralError.class, () -> OpenFeatureAPI.getInstance().setProviderAndWait(provider));

assertTrue(System.currentTimeMillis() - started < 1000);
assertEquals(ProviderState.NOT_READY, provider.getState());
}

@Test
public void itDoesNotWaitWithAStartWaitTimeOfZero() {
var config = new LDConfig.Builder()
.dataSource(new DelayedDataSourceFactory(Duration.ofSeconds(30), false))
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config, Duration.ZERO);

var started = System.currentTimeMillis();
assertThrows(GeneralError.class, () -> OpenFeatureAPI.getInstance().setProviderAndWait(provider));

assertTrue(System.currentTimeMillis() - started < 1000);
}
}
Loading