From 83a1dcf9070b2720b58acfd22f41e751bad804ed 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/4] feat: Add a start wait timeout for initialization Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 2 ++ ld_openfeature/provider.py | 17 ++++++++++++++--- tests/test_data_sources.py | 17 +++++++++++++++++ tests/test_provider.py | 36 ++++++++++++++++++++++++++++++++++-- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f829dd8..5d7a124 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ api.set_provider(openfeature_provider) # Refer to OpenFeature documentation for getting a client and performing evaluations. ``` +The optional `start_wait` parameter controls how long initialization waits in seconds. Its default matches the LaunchDarkly SDK default, and zero means no timeout. + Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK. For information on using the OpenFeature client please refer to the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/). diff --git a/ld_openfeature/provider.py b/ld_openfeature/provider.py index 4539378..de26b5e 100644 --- a/ld_openfeature/provider.py +++ b/ld_openfeature/provider.py @@ -22,8 +22,16 @@ class LaunchDarklyProvider(AbstractProvider): - def __init__(self, config: Config): - self.__client = LDClient(config) + def __init__(self, config: Config, start_wait: float = 5): + """ + Create a provider backed by a LaunchDarkly client. + + :param config: The LaunchDarkly client configuration. + :param start_wait: The maximum time in seconds to wait for initialization; zero means no timeout. + The default matches the LaunchDarkly SDK default. + """ + self.__client = LDClient(config, start_wait) + self.__start_wait = start_wait self.__context_converter = EvaluationContextConverter() self.__details_converter = ResolutionDetailsConverter() @@ -80,7 +88,10 @@ def ready_handler(status: DataSourceStatus): if self.__client.is_initialized(): ready_event.set() - ready_event.wait() + if self.__start_wait > 0: + ready_event.wait(self.__start_wait) + else: + ready_event.wait() self.__client.data_source_status_provider.remove_listener(ready_handler) diff --git a/tests/test_data_sources.py b/tests/test_data_sources.py index 2440083..1deadb1 100644 --- a/tests/test_data_sources.py +++ b/tests/test_data_sources.py @@ -40,6 +40,23 @@ def initialized(self): return False +class NeverReadyDataSource(UpdateProcessor): + def __init__(self, config: Config, store, ready: threading.Event): + self._ready = ready + + def start(self): + pass + + def stop(self): + pass + + def is_alive(self): + return False + + def initialized(self): + return False + + class DelayedFailingDataSource(UpdateProcessor): def __init__(self, config: Config, store, ready: threading.Event): self._data_source_update_sink: Optional[DataSourceUpdateSink] = config.data_source_update_sink diff --git a/tests/test_provider.py b/tests/test_provider.py index 5271c88..e685b9f 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -9,14 +9,14 @@ from ldclient.integrations.test_data import TestData from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEvent, EventDetails -from openfeature.exception import ErrorCode +from openfeature.exception import ErrorCode, ProviderFatalError from openfeature.flag_evaluation import Reason from openfeature.provider import ProviderStatus from openfeature.track import TrackingEventDetails from openfeature import api from ld_openfeature import LaunchDarklyProvider, Config -from tests.test_data_sources import FailingDataSource, InitializedThenFailingDataSource, StaleDataSource, UpdatingDataSource, DelayedFailingDataSource +from tests.test_data_sources import FailingDataSource, InitializedThenFailingDataSource, NeverReadyDataSource, StaleDataSource, UpdatingDataSource, DelayedFailingDataSource @pytest.fixture @@ -49,6 +49,38 @@ def test_ldclient_is_accessible(provider: LaunchDarklyProvider): assert type(provider.client) is LDClient +def test_default_start_wait_matches_launchdarkly_sdk_default(): + config = Config("", offline=True) + + with patch("ld_openfeature.provider.LDClient") as client: + LaunchDarklyProvider(config) + + client.assert_called_once_with(config, 5) + + +def test_initialization_times_out_with_positive_start_wait(): + result = {} + + def initialize_provider(): + provider = LaunchDarklyProvider( + Config("", update_processor_class=NeverReadyDataSource, send_events=False), + start_wait=0.1, + ) + result["provider"] = provider + try: + provider.initialize(EvaluationContext("user-key")) + except Exception as error: + result["error"] = error + + thread = threading.Thread(target=initialize_provider, daemon=True) + thread.start() + thread.join(timeout=1) + + assert not thread.is_alive() + assert isinstance(result["error"], ProviderFatalError) + result["provider"].shutdown() + + def test_not_providing_context_returns_error(provider: LaunchDarklyProvider): resolution_details = provider.resolve_boolean_details("flag-key", True, None) From 7afe07323daf2d63a08ad6539839b0b41818175e 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:43:02 +0000 Subject: [PATCH 2/4] docs: Clarify the blocking behavior of start_wait Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 2 +- ld_openfeature/provider.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5d7a124..f5bfc69 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ api.set_provider(openfeature_provider) # Refer to OpenFeature documentation for getting a client and performing evaluations. ``` -The optional `start_wait` parameter controls how long initialization waits in seconds. Its default matches the LaunchDarkly SDK default, and zero means no timeout. +The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value blocks the provider constructor for up to that long, and bounds how long OpenFeature initialization waits before reporting a failed initialization. Zero does not block the constructor at all, and initialization then waits without a deadline for the data source to become valid or to fail permanently. Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK. diff --git a/ld_openfeature/provider.py b/ld_openfeature/provider.py index de26b5e..53b3257 100644 --- a/ld_openfeature/provider.py +++ b/ld_openfeature/provider.py @@ -27,8 +27,11 @@ def __init__(self, config: Config, start_wait: float = 5): Create a provider backed by a LaunchDarkly client. :param config: The LaunchDarkly client configuration. - :param start_wait: The maximum time in seconds to wait for initialization; zero means no timeout. - The default matches the LaunchDarkly SDK default. + :param start_wait: The number of seconds to wait for a successful connection to LaunchDarkly, matching + the same parameter of :class:`ldclient.LDClient`. A positive value blocks this constructor for up to + that long, and bounds how long ``initialize`` waits before reporting a failed initialization. Zero + does not block this constructor at all, and ``initialize`` then waits without a deadline for the + data source to become valid or to fail permanently. """ self.__client = LDClient(config, start_wait) self.__start_wait = start_wait From 057a27b9746e4a87a3b148c8e9a8ba2d9efc6a25 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:58:50 +0000 Subject: [PATCH 3/4] 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 +- ld_openfeature/provider.py | 14 +++++++------- tests/test_provider.py | 33 ++++++++++++--------------------- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index f5bfc69..82c3973 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ api.set_provider(openfeature_provider) # Refer to OpenFeature documentation for getting a client and performing evaluations. ``` -The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value blocks the provider constructor for up to that long, and bounds how long OpenFeature initialization waits before reporting a failed initialization. Zero does not block the constructor at all, and initialization then waits without a deadline for the data source to become valid or to fail permanently. +The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value bounds the whole of initialization: the provider constructor blocks for up to that long, and OpenFeature initialization then completes immediately, reporting a failed initialization if the client did not become ready in time. Zero does not block the constructor at all, and initialization then waits without a deadline for the data source to become valid or to fail permanently. Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK. diff --git a/ld_openfeature/provider.py b/ld_openfeature/provider.py index 53b3257..397c1d1 100644 --- a/ld_openfeature/provider.py +++ b/ld_openfeature/provider.py @@ -28,10 +28,11 @@ def __init__(self, config: Config, start_wait: float = 5): :param config: The LaunchDarkly client configuration. :param start_wait: The number of seconds to wait for a successful connection to LaunchDarkly, matching - the same parameter of :class:`ldclient.LDClient`. A positive value blocks this constructor for up to - that long, and bounds how long ``initialize`` waits before reporting a failed initialization. Zero - does not block this constructor at all, and ``initialize`` then waits without a deadline for the - data source to become valid or to fail permanently. + the same parameter of :class:`ldclient.LDClient`. A positive value bounds the whole of initialization: + this constructor blocks for up to that long, and ``initialize`` then completes immediately, reporting + a failed initialization if the client did not become ready in time. Zero does not block this + constructor at all, and ``initialize`` then waits without a deadline for the data source to become + valid or to fail permanently. """ self.__client = LDClient(config, start_wait) self.__start_wait = start_wait @@ -91,9 +92,8 @@ def ready_handler(status: DataSourceStatus): if self.__client.is_initialized(): ready_event.set() - if self.__start_wait > 0: - ready_event.wait(self.__start_wait) - else: + # With a start wait the client constructor has already waited, so the outcome is whatever it is now. + if self.__start_wait <= 0: ready_event.wait() self.__client.data_source_status_provider.remove_listener(ready_handler) diff --git a/tests/test_provider.py b/tests/test_provider.py index e685b9f..a6690ff 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -58,27 +58,18 @@ def test_default_start_wait_matches_launchdarkly_sdk_default(): client.assert_called_once_with(config, 5) -def test_initialization_times_out_with_positive_start_wait(): - result = {} - - def initialize_provider(): - provider = LaunchDarklyProvider( - Config("", update_processor_class=NeverReadyDataSource, send_events=False), - start_wait=0.1, - ) - result["provider"] = provider - try: - provider.initialize(EvaluationContext("user-key")) - except Exception as error: - result["error"] = error - - thread = threading.Thread(target=initialize_provider, daemon=True) - thread.start() - thread.join(timeout=1) - - assert not thread.is_alive() - assert isinstance(result["error"], ProviderFatalError) - result["provider"].shutdown() +def test_initialization_fails_without_waiting_again_with_positive_start_wait(): + provider = LaunchDarklyProvider( + Config("", update_processor_class=NeverReadyDataSource, send_events=False), + start_wait=0.5, + ) + + started = time.time() + with pytest.raises(ProviderFatalError): + provider.initialize(EvaluationContext("user-key")) + + assert time.time() - started < 0.25 + provider.shutdown() def test_not_providing_context_returns_error(provider: LaunchDarklyProvider): From 69390f77079c746f29f749b0ae5195c3f1f5e619 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:03:01 +0000 Subject: [PATCH 4/4] docs: Mark initialization as supported in the feature matrix Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14416f6..ba6d05c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Python](https | ✅ | 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 as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. | | ✅ | Tracking | `track` sends a LaunchDarkly custom event for the evaluation context, with the tracking event value and remaining details attached. | -| ⚠️ | Initialization | `initialize` reports whether the LaunchDarkly client became ready. It has no timeout of its own and waits until the data source becomes valid or permanently fails: [#55](https://github.com/launchdarkly/openfeature-python-server/issues/55). | +| ✅ | Initialization | `initialize` reports whether the LaunchDarkly client became ready. The optional `start_wait` parameter bounds initialization; zero applies no timeout and waits until the data source becomes valid or permanently fails. | | ✅ | 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 | The underlying LaunchDarkly client is available through the `client` property. |