Repository navigation
feat!: Add a start wait time option for initialization - #66
Conversation
Fixes #58 Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
|
@cursor review |
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Gives the OpenFeature providers the initialization waiting values the OpenFeature provider spec (OFP) requires, which the numeric-only `initTimeoutSeconds` could not express. - `initTimeoutSeconds: 'forever'` waits indefinitely for the LaunchDarkly client - `initTimeoutSeconds: 0` waits nowhere and fails initialization unless the client is already ready, so readiness is observed through provider events - A positive timeout behaves as before, and the default is still 10 seconds - Node provider README feature matrix row updated for the new values <details> <summary>Implementation details</summary> **Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** OFP requirement 4.3.4 requires a value distinct from any duration to request waiting indefinitely, and 4.3.6 requires a zero wait to wait nowhere and fail initialization. Sibling changes for the same requirement: launchdarkly/openfeature-java-server#66, launchdarkly/openfeature-python-server#64, launchdarkly/openfeature-dotnet-server#71, launchdarkly/openfeature-ruby-server#36. **Describe the solution you've provided** `BaseProviderConfig.initTimeoutSeconds` and the Node provider's constructor parameter are now `number | 'forever'`, and zero is no longer coalesced into the default, since `??` only replaces `undefined`. `waitForInitialization` treats a falsy timeout as no timeout, so zero cannot be passed through: initialization races the pending initialization promise against an immediately rejected one, which succeeds only when the client is already ready. The Cloudflare provider is unaffected because it evaluates from a KV namespace and has no data source to wait for. **Describe alternatives you've considered** `number | null` was used initially for the indefinite case; `'forever'` reads better at the call site and matches the repository's preference for string unions. Adding `initialized()` to the client contract would let the zero case be checked directly, but that is a breaking change to a published interface for behavior the existing promise already expresses. **Additional context** Verified with `yarn workspace @launchdarkly/openfeature-js-server-common test` (82 tests), `yarn workspace @launchdarkly/openfeature-node-server test` (29 tests), and lint, format and build checks for both packages. </details> Link to Devin session: https://app.devin.ai/sessions/1fd3fcbfe79f482e8b58be29e8df2ffb Open in Devin Desktop: https://app.devin.ai/desktop/session/1fd3fcbfe79f482e8b58be29e8df2ffb?variant=devin Requested by: @kinyoklion <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Extends **`initTimeoutSeconds`** on the shared **`BaseOpenFeatureProvider`** and the Node **`LaunchDarklyProvider`** to **`number | 'forever'`**, aligning initialization with the OpenFeature provider spec. > > **`'forever'`** calls **`waitForInitialization()`** with no timeout. **`0`** no longer falls through to the 10-second default (only **`undefined`** does); initialization races the client’s init promise against an immediate failure so it succeeds only when the client is already ready, otherwise it throws a clear error. Positive timeouts behave as before (default **10** seconds). > > The Node provider README initialization row documents the new values, and unit tests cover indefinite wait, zero-timeout failure, and zero-timeout success when already ready. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 87e9a23. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Devin check the equivalent .Net PR. I would prefer we have something like Duration.Infinite instead of null. |
… waiting Co-Authored-By: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
…-start-wait Co-Authored-By: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> # Conflicts: # src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java
…letes Co-Authored-By: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
|
Matched the .NET PR, which uses Also merged |
…tate Co-Authored-By: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Reworks the start wait time behavior in response to review: - An indefinite initialization (Provider.INFINITE_START_WAIT) can now be cancelled: shutdown() completes the initialization future, and the interrupt flag is restored if the waiting thread is interrupted. Documents that a direct provider.shutdown() (from another thread) or the non-blocking setProvider is required to end such a wait, because OpenFeatureAPI.shutdown() would block on the API lock setProviderAndWait holds for the duration of the wait. - Classifies any start wait time too large to represent in milliseconds, including INFINITE_START_WAIT, as indefinite, so it saturates instead of overflowing when the SDK converts it. A null start wait time falls through to the SDK's default rather than throwing. - A failed initialization carries the data source's last error into the thrown exception, so the OpenFeature error event reports the cause. - Does not report READY from the client's initialized latch while the data source is OFF. - Keeps emitting one stale event per state change, and stale events carry the HTTP error detail. - shutdown() removes its listeners and terminates the event emitter executor. Adds lifecycle tests for these paths and makes start-wait classification directly unit-testable.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2ef5a1a. Configure here.
…the provider Reworks shutdown and initialization in response to review: - shutdown() no longer completes the initialization future or removes listeners itself. Closing the client transitions the data source to OFF, which the still-registered status listener handles under the lock to record the terminal state and end an indefinite initialization. Cancellation runs through the same path as any other status change, so there is no separate, unlocked write during shutdown. - A client that has ever been initialized is reported READY again, rather than being excluded when its data source is currently OFF; the initialized latch is the signal that cached data can be served. - The provider does not suppress ready/stale events during teardown; it reflects the data source status the SDK reports. The shutdown flag is only used to classify the client's own close as an expected teardown rather than a provider error. Listeners are registered inline and intentionally kept through close so the OFF status can be observed. Tests updated accordingly.
|
AI review comment: addressing the two Cursor Bugbot findings after the latest rework (commit 1c96e64). "Shutdown races with status updates" — addressed. "Race can drop ready event" — known, not reproduced. This is a pre-existing theoretical window on the bounded-wait path (fabricated-ERROR vs a concurrent VALID), not introduced or changed by this rework. An adversarial review attempted to reproduce it with a 400-iteration timing sweep and could not: the OpenFeature status write lands synchronously on the initializing thread while the provider's event is dispatched on a separate executor, so the newest status wins. Leaving it as a documented, unproven window rather than adding speculative synchronization. |

Requirements
Related issues
Fixes #58.
Describe the solution you've provided
Adds
Provider(String sdkKey, LDConfig config, Duration startWait)so an application can bound, or deliberately opt out of, waiting for initialization, matching the OpenFeature provider spec (OFP) requirements for start wait behavior:Durationis applied by the LaunchDarkly client constructor, andinitializethen reports whether the client became ready without waiting a second timeDuration.ZEROwaits nowhere;initializefails unless the client is already ready, and the application learns when it becomes usable from provider eventsProvider.INFINITE_START_WAITrequests indefinite waiting: nothing is waited for during construction andinitializeblocks until the data source becomes valid or fails permanentlyThe existing constructors keep honouring
LDConfig.Builder.startWait(...)and no longer wait again ininitialize, so a configured start wait is the only bound on initialization instead of being followed by an unbounded wait. The README feature matrix row and initialization section describe the new behavior.Implementation details
initializestill registers data source and flag change listeners and evaluates the current data source status. Only the indefinite mode leaves the completion future pending; every other mode completes it with the outcome that is already known, soinitializereturns promptly.The OpenFeature SDK emits its own event for the transition that completes initialization, so the provider records that one state without emitting a duplicate, and emits every state change after it — including a permanent failure that arrives immediately after the data source became valid, and a recovery after a failed initialization.
Describe alternatives you've considered
Keeping the existing indefinite wait and adding a timeout parameter interpreted only inside
initializewould double-wait whenLDConfig.startWaitis also set, and would not match OFP, which treats the start wait as a bound over construction plus initialization.Java has no built-in infinite
Duration, so indefinite waiting was initially expressed asnull. It is now the explicitProvider.INFINITE_START_WAITconstant, matchingTimeout.InfiniteTimeSpanin the .NET provider.Additional context
Sibling changes for the same OFP requirement: launchdarkly/openfeature-python-server#64, launchdarkly/openfeature-ruby-server#36, launchdarkly/openfeature-dotnet-server#71, and launchdarkly/js-core#2024.
Verified with
./gradlew test(all tests pass, including new coverage for positive, zero, and indefinite start wait).Link to Devin session: https://app.devin.ai/sessions/1fd3fcbfe79f482e8b58be29e8df2ffb
Open in Devin Desktop: https://app.devin.ai/desktop/session/1fd3fcbfe79f482e8b58be29e8df2ffb?variant=devin
Requested by: @kinyoklion
SDK-3285
Note
Overview
Fixes OpenFeature initialization behavior (#58) by adding
Provider(String, LDConfig, Duration startWait)andProvider.INFINITE_START_WAIT, so startup waiting is bounded in one place instead of blocking again indefinitely insideinitialize()afterLDConfig.startWait.Positive durations apply during client construction;
Duration.ZEROfails fast unless already ready; indefinite mode skips construction blocking and waits ininitialize()until the data source is valid, fails, or the provider shuts down. Oversized durations are treated as indefinite to avoid SDK millis overflow.initialize()is reworked around a completion future and tighter state/event handling: failed init surfaces data-source errors, duplicate STALE/READY events are deduped, teardown OFF does not emit PROVIDER_ERROR, andshutdown()callssuper.shutdown()to stop leaking the event executor. README documents cancellation of indefinitesetProviderAndWait(directprovider.shutdown()vs API lock contention).Lifecycle tests cover bounded/zero/infinite waits, recovery, and classification boundaries.
Reviewed by Cursor Bugbot for commit 1c96e64. Bugbot is set up for automated code reviews on this repo. Configure here.
tanderson-ld additional changes
Review-driven hardening of the start wait time behavior, on top of the commits above. Summary of the delta:
Indefinite wait is now cancellable.
shutdown()completes the initialization future, and the interrupt flag is restored if the waiting thread is interrupted. The README and constructor javadoc document how to cancel an indefinitesetProviderAndWait: callprovider.shutdown()directly from another thread, or register with the non-blockingsetProvider.OpenFeatureAPI.shutdown()cannot end such a wait — it blocks on the same API locksetProviderAndWaitholds for the duration of initialization. This lock-contention deadlock is inherent to the OpenFeature Java SDK (it holds the API write lock across a synchronousinitialize()and contends on it inshutdown()), so it is documented rather than worked around; an upstream issue will be filed against the OpenFeature Java SDK.Start wait time classification / validation.
Provider.INFINITE_START_WAITand any other "forever" duration — is classified as indefinite, so it saturates to the indefinite path instead of overflowing when the SDK converts it to milliseconds. (Separately, a too-large duration configured throughLDConfig.Builder.startWaitinstead of the constructor still overflows inside the SDK client constructor and leaks the client; that is pre-existing SDK behavior and has been filed as a separate java-core ticket. The sentinel is documented as constructor-argument-only.)nullstart wait time falls through to the SDK's default start wait rather than throwing.Status / event fidelity.
isInitialized()latch no longer reportsREADYwhen the data source is currentlyOFF.PROVIDER_STALEper state change (state-based dedupe, retained and documented), and stale events now carry the HTTP error detail.Lifecycle cleanup.
shutdown()removes its flag-change and data-source status listeners and callssuper.shutdown()to terminate the event emitter executor.Tests. Added lifecycle coverage for bounded / zero / indefinite waits, failed-init-then-recovery, interruption → stale → ready, single-stale-per-outage dedupe, cancellation of an indefinite
setProviderAndWaitvia a directprovider.shutdown(), theOFF-while-initialized exclusion, listener removal on shutdown, and a direct unit test of the start-wait classification boundary. Full suite and checkstyle pass.