From b5ce6a10fbaa734b700a0cc482eac614663eda64 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:59:10 +0000 Subject: [PATCH 1/7] feat: Add a start wait timeout for initialization Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 2 +- .../openfeature/serverprovider/Provider.java | 40 ++++++++++++-- .../serverprovider/LifeCycleTest.java | 54 +++++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dc10c94..c8c831f 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ 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`. The `Provider(String, LDConfig, Duration)` constructor can also configure the initialization start wait; a zero duration means no timeout. 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 3031d61..19c68e6 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -11,9 +11,12 @@ 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; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java @@ -31,6 +34,11 @@ * */ public class Provider extends EventProvider { + /** + * The Java SDK's default start wait. + */ + private static final Duration DEFAULT_START_WAIT = Duration.ofSeconds(5); + private static final class ProviderMetaData implements Metadata { @Override public String getName() { @@ -46,6 +54,7 @@ public String getName() { private final EvaluationContextConverter evaluationContextConverter; private final LDClientInterface client; + private final Duration startWait; private ProviderState state = ProviderState.NOT_READY; @@ -59,7 +68,7 @@ public String getName() { * @param sdkKey the SDK key for your LaunchDarkly environment */ public Provider(String sdkKey) { - this(sdkKey, new LDConfig.Builder().build()); + this(sdkKey, new LDConfig.Builder().build(), DEFAULT_START_WAIT); } /** @@ -69,14 +78,31 @@ public Provider(String sdkKey) { * @param config a client configuration object */ public Provider(String sdkKey, LDConfig config) { + this(sdkKey, config, DEFAULT_START_WAIT); + } + + /** + * Create a provider with the specified SDK key, configuration, and start wait timeout. + * + * @param sdkKey the SDK key for your LaunchDarkly environment + * @param config a client configuration object + * @param startWait the maximum duration to wait for initialization; zero means no timeout + */ + public Provider(String sdkKey, LDConfig config, Duration startWait) { this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config) + .startWait(startWait) .wrapper(Components.wrapperInfo() .wrapperName("open-feature-java-server") - .wrapperVersion(Version.SDK_VERSION)).build())); + .wrapperVersion(Version.SDK_VERSION)).build()), startWait); } Provider(LDClientInterface client) { + this(client, Duration.ZERO); + } + + Provider(LDClientInterface client, Duration startWait) { this.client = client; + this.startWait = startWait; logger = client.getLogger(); evaluationContextConverter = new EvaluationContextConverter(logger); evaluationDetailConverter = new EvaluationDetailConverter(logger); @@ -168,7 +194,15 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer); - var successfullyInitialized = completer.get(); + boolean successfullyInitialized; + try { + successfullyInitialized = startWait.isZero() + ? completer.get() + : completer.get(startWait.toMillis(), TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + setState(ProviderState.ERROR); + throw new RuntimeException("Wait for initialization timed out.", e); + } if(!successfullyInitialized) { throw new RuntimeException("Failed to initialize LaunchDarkly client."); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index 5f4304f..ee05eb1 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -32,6 +32,8 @@ 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.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; class DelayedDataSource implements DataSource { @@ -98,6 +100,26 @@ public DataSource build(ClientContext clientContext) { } } +class NeverReadyDataSource implements DataSource { + public Future start() { + return new CompletableFuture<>(); + } + + public boolean isInitialized() { + return false; + } + + public void close() throws IOException { + } +} + +class NeverReadyDataSourceFactory implements ComponentConfigurer { + @Override + public DataSource build(ClientContext clientContext) { + return new NeverReadyDataSource(); + } +} + /** * Tests in this suite use a real client instance and the public constructor. *

@@ -221,4 +243,36 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS)); } + + @Test + public void initializationTimesOutWhenStartWaitIsPositive() { + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> { + var config = new LDConfig.Builder() + .dataSource(new NeverReadyDataSourceFactory()) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config, Duration.ofMillis(100)); + try { + var error = assertThrows(RuntimeException.class, + () -> provider.initialize(new ImmutableContext("context-key"))); + assertTrue(error.getMessage().contains("Wait for initialization timed out.")); + assertEquals(ProviderState.ERROR, provider.getState()); + } finally { + provider.shutdown(); + } + }); + } + + @Test + public void initializationWaitsIndefinitelyWhenStartWaitIsZero() throws Exception { + var config = new LDConfig.Builder() + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(200), false)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config, Duration.ZERO); + + assertDoesNotThrow(() -> provider.initialize(new ImmutableContext("context-key"))); + assertEquals(ProviderState.READY, provider.getState()); + provider.shutdown(); + } } From a56a6f3afa38bb26b0a5ed1536fb97146ba2a78e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:01:15 +0000 Subject: [PATCH 2/7] fix: Preserve configured Java start wait Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 2 +- .../openfeature/serverprovider/Provider.java | 28 +++++++++++-------- .../serverprovider/LifeCycleTest.java | 13 +++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c8c831f..fb85cca 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ 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 `Provider(String, LDConfig, Duration)` constructor can also configure the initialization start wait; a zero duration means no timeout. +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 `Provider(String, LDConfig, Duration)` constructor configures both the SDK's start wait and the provider's initialization timeout; a zero duration means no timeout. The other constructors leave the config's start wait untouched, and the provider waits indefinitely for the data source to become valid or permanently fail. 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 19c68e6..36c24dc 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -34,11 +34,6 @@ * */ public class Provider extends EventProvider { - /** - * The Java SDK's default start wait. - */ - private static final Duration DEFAULT_START_WAIT = Duration.ofSeconds(5); - private static final class ProviderMetaData implements Metadata { @Override public String getName() { @@ -68,32 +63,41 @@ public String getName() { * @param sdkKey the SDK key for your LaunchDarkly environment */ public Provider(String sdkKey) { - this(sdkKey, new LDConfig.Builder().build(), DEFAULT_START_WAIT); + this(new LDClient(sdkKey, withWrapper(new LDConfig.Builder().build())), Duration.ZERO); } /** - * Crate a provider with the specified SDK key and configuration. + * Create a provider with the specified SDK key and configuration. * * @param sdkKey the SDK key for your LaunchDarkly environment - * @param config a client configuration object + * @param config a client configuration object; its start wait setting is preserved, and provider initialization + * waits indefinitely */ public Provider(String sdkKey, LDConfig config) { - this(sdkKey, config, DEFAULT_START_WAIT); + this(new LDClient(sdkKey, withWrapper(config)), Duration.ZERO); } /** - * Create a provider with the specified SDK key, configuration, and start wait timeout. + * Create a provider with the specified SDK key, configuration, and start wait timeout. This configures both the + * LaunchDarkly SDK's start wait and the provider's initialization timeout. * * @param sdkKey the SDK key for your LaunchDarkly environment * @param config a client configuration object - * @param startWait the maximum duration to wait for initialization; zero means no timeout + * @param startWait the maximum duration to wait for initialization; zero means no provider-applied timeout */ public Provider(String sdkKey, LDConfig config, Duration startWait) { this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config) .startWait(startWait) .wrapper(Components.wrapperInfo() .wrapperName("open-feature-java-server") - .wrapperVersion(Version.SDK_VERSION)).build()), startWait); + .wrapperVersion(Version.SDK_VERSION)).build()), startWait); + } + + private static LDConfig withWrapper(LDConfig config) { + return LDConfig.Builder.fromConfig(config) + .wrapper(Components.wrapperInfo() + .wrapperName("open-feature-java-server") + .wrapperVersion(Version.SDK_VERSION)).build(); } Provider(LDClientInterface client) { diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index ee05eb1..8e05382 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -161,6 +161,19 @@ public void canShutdownAnOfflineClient() { }); } + @Test + public void twoArgumentConstructorPreservesConfigStartWait() { + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> { + var config = new LDConfig.Builder() + .startWait(Duration.ZERO) + .dataSource(new NeverReadyDataSourceFactory()) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config); + provider.shutdown(); + }); + } + @Test public void itEmitsReadyEvents() throws ExecutionException, InterruptedException, TimeoutException { var provider = new Provider("fake-key", new LDConfig.Builder() From 294a4a59987065b584e032cf0bca86b88fcbcbd8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:01:58 +0000 Subject: [PATCH 3/7] refactor: Reuse the wrapper config helper in the start wait constructor Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../launchdarkly/openfeature/serverprovider/Provider.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 36c24dc..c959080 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -86,11 +86,8 @@ public Provider(String sdkKey, LDConfig config) { * @param startWait the maximum duration to wait for initialization; zero means no provider-applied timeout */ public Provider(String sdkKey, LDConfig config, Duration startWait) { - this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config) - .startWait(startWait) - .wrapper(Components.wrapperInfo() - .wrapperName("open-feature-java-server") - .wrapperVersion(Version.SDK_VERSION)).build()), startWait); + this(new LDClient(sdkKey, + withWrapper(LDConfig.Builder.fromConfig(config).startWait(startWait).build())), startWait); } private static LDConfig withWrapper(LDConfig config) { From b8e23e18d1f3507fed2c84f4ffa1f040935fee10 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:36:59 +0000 Subject: [PATCH 4/7] docs: Recommend bounded initialization waits Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../com/launchdarkly/openfeature/serverprovider/Provider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index c959080..9140361 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -59,6 +59,8 @@ public String getName() { * Create a provider with the specified SDK and default configuration. *

* If you need to specify any configuration use {@link Provider#Provider(String, LDConfig)} instead. + * Initialization waits indefinitely; use {@link Provider#Provider(String, LDConfig, Duration)} when a bounded wait + * is wanted. * * @param sdkKey the SDK key for your LaunchDarkly environment */ @@ -68,6 +70,8 @@ public Provider(String sdkKey) { /** * Create a provider with the specified SDK key and configuration. + * Initialization waits indefinitely; use {@link Provider#Provider(String, LDConfig, Duration)} when a bounded wait + * is wanted. * * @param sdkKey the SDK key for your LaunchDarkly environment * @param config a client configuration object; its start wait setting is preserved, and provider initialization From 095f4ea2faf4af2ade8b437c4b41b07fee6801a1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 21:57:50 +0000 Subject: [PATCH 5/7] fix: Resolve initialization immediately when a start wait was used Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 2 +- .../openfeature/serverprovider/Provider.java | 28 ++++++++--------- .../openfeature/serverprovider/Version.java | 2 +- .../serverprovider/LifeCycleTest.java | 31 ++++++++++++++++--- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index fb85cca..032d773 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ 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 `Provider(String, LDConfig, Duration)` constructor configures both the SDK's start wait and the provider's initialization timeout; a zero duration means no timeout. The other constructors leave the config's start wait untouched, and the provider waits indefinitely for the data source to become valid or permanently fail. +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 `Provider(String, LDConfig, Duration)` constructor bounds the whole of initialization with that duration: the constructor blocks for up to that long, and `initialize` then completes immediately with whatever the outcome was, failing if the client did not become ready in time. A zero duration means the provider waits indefinitely instead. The other constructors leave the config's start wait untouched, and the provider waits indefinitely for the data source to become valid or permanently fail. 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 9140361..764ce0b 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -15,8 +15,6 @@ import java.util.Collections; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; /** * An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java @@ -82,12 +80,13 @@ public Provider(String sdkKey, LDConfig config) { } /** - * Create a provider with the specified SDK key, configuration, and start wait timeout. This configures both the - * LaunchDarkly SDK's start wait and the provider's initialization timeout. + * Create a provider with the specified SDK key, configuration, and start wait duration. The duration bounds the + * whole of initialization: the constructor blocks for up to that long, and initialization then completes with + * whatever the outcome was, rather than waiting again. * * @param sdkKey the SDK key for your LaunchDarkly environment * @param config a client configuration object - * @param startWait the maximum duration to wait for initialization; zero means no provider-applied timeout + * @param startWait the maximum duration to wait for initialization; zero means wait indefinitely */ public Provider(String sdkKey, LDConfig config, Duration startWait) { this(new LDClient(sdkKey, @@ -199,17 +198,18 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer); - boolean successfullyInitialized; - try { - successfullyInitialized = startWait.isZero() - ? completer.get() - : completer.get(startWait.toMillis(), TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - setState(ProviderState.ERROR); - throw new RuntimeException("Wait for initialization timed out.", e); + + // With a start wait the client constructor has already waited, so the data source has either become valid, + // failed permanently, or run out of time; the outcome is whatever it is now. + if (!startWait.isZero()) { + if (!completer.getNow(false)) { + setState(ProviderState.ERROR); + throw new RuntimeException("The client did not initialize within the start wait duration."); + } + return; } - if(!successfullyInitialized) { + if (!completer.get()) { throw new RuntimeException("Failed to initialize LaunchDarkly client."); } } diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java index ffd52fc..c842e08 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java @@ -4,5 +4,5 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed - static final String SDK_VERSION = "1.0.1"; + static final String SDK_VERSION = "1.1.3"; } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index 8e05382..232129f 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -258,17 +258,21 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E } @Test - public void initializationTimesOutWhenStartWaitIsPositive() { + public void initializationFailsWithoutWaitingAgainWhenStartWaitIsPositive() { assertTimeoutPreemptively(Duration.ofSeconds(1), () -> { var config = new LDConfig.Builder() .dataSource(new NeverReadyDataSourceFactory()) .events(Components.noEvents()) .build(); - var provider = new Provider("fake-key", config, Duration.ofMillis(100)); + var provider = new Provider("fake-key", config, Duration.ofMillis(300)); try { - var error = assertThrows(RuntimeException.class, - () -> provider.initialize(new ImmutableContext("context-key"))); - assertTrue(error.getMessage().contains("Wait for initialization timed out.")); + // The constructor consumed the start wait, so initialization must not wait a second time. + assertTimeoutPreemptively(Duration.ofMillis(100), () -> { + var error = assertThrows(RuntimeException.class, + () -> provider.initialize(new ImmutableContext("context-key"))); + assertTrue(error.getMessage() + .contains("The client did not initialize within the start wait duration.")); + }); assertEquals(ProviderState.ERROR, provider.getState()); } finally { provider.shutdown(); @@ -276,6 +280,23 @@ public void initializationTimesOutWhenStartWaitIsPositive() { }); } + @Test + public void initializationSucceedsWhenTheClientBecomesReadyDuringTheStartWait() { + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> { + var config = new LDConfig.Builder() + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config, Duration.ofMillis(500)); + try { + assertDoesNotThrow(() -> provider.initialize(new ImmutableContext("context-key"))); + assertEquals(ProviderState.READY, provider.getState()); + } finally { + provider.shutdown(); + } + }); + } + @Test public void initializationWaitsIndefinitelyWhenStartWaitIsZero() throws Exception { var config = new LDConfig.Builder() From 70082f7c2ef38003de3dd2a5e847c92f1e165506 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 21:59:39 +0000 Subject: [PATCH 6/7] chore: Restore the generated version constant Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../com/launchdarkly/openfeature/serverprovider/Version.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java index c842e08..ffd52fc 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java @@ -4,5 +4,5 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed - static final String SDK_VERSION = "1.1.3"; + static final String SDK_VERSION = "1.0.1"; } From 8862fe7ca54972cb2b9c119b337d65a131a5f33d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:06:12 +0000 Subject: [PATCH 7/7] fix: Distinguish a permanent initialization failure from a lapsed start wait Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../openfeature/serverprovider/Provider.java | 9 +++------ .../serverprovider/LifeCycleTest.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 764ce0b..50a2fc8 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -201,12 +201,9 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { // With a start wait the client constructor has already waited, so the data source has either become valid, // failed permanently, or run out of time; the outcome is whatever it is now. - if (!startWait.isZero()) { - if (!completer.getNow(false)) { - setState(ProviderState.ERROR); - throw new RuntimeException("The client did not initialize within the start wait duration."); - } - return; + if (!startWait.isZero() && !completer.isDone()) { + setState(ProviderState.ERROR); + throw new RuntimeException("The client did not initialize within the start wait duration."); } if (!completer.get()) { diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index 232129f..b935af7 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -297,6 +297,24 @@ public void initializationSucceedsWhenTheClientBecomesReadyDuringTheStartWait() }); } + @Test + public void initializationReportsPermanentFailureAfterAPositiveStartWait() { + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> { + var config = new LDConfig.Builder() + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), true)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config, Duration.ofMillis(500)); + try { + var error = assertThrows(RuntimeException.class, + () -> provider.initialize(new ImmutableContext("context-key"))); + assertEquals("Failed to initialize LaunchDarkly client.", error.getMessage()); + } finally { + provider.shutdown(); + } + }); + } + @Test public void initializationWaitsIndefinitelyWhenStartWaitIsZero() throws Exception { var config = new LDConfig.Builder()