diff --git a/README.md b/README.md index 1a6e57f..31a4a47 100644 --- a/README.md +++ b/README.md @@ -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. | @@ -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. diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index b83e3d9..794811b 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -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; @@ -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. *
@@ -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. + *
+ * 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);
@@ -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);
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java
index 929b204..8f9141d 100644
--- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java
+++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java
@@ -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 {
@@ -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();
@@ -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