Skip to content

Commit c8708fd

Browse files
feat: Add a start wait timeout for initialization (#59)
Adds a `start_wait` parameter to `LaunchDarklyProvider`, defaulting to the LaunchDarkly SDK's five seconds. Closes [#55](#55). - The value is passed to `LDClient(config, start_wait)` and bounds the whole of initialization once: with a positive value `initialize` reports the outcome the constructor already waited for rather than waiting again, so a five second start wait cannot become a ten second wait for `set_provider_and_wait`. - Initialization fails when the client did not become ready in time; the provider keeps reporting status afterward, so a later connection still makes it ready. - Zero does not block the constructor at all, and `initialize` then waits without a deadline for the data source to become valid or to fail permanently. - Flips the README feature matrix's Initialization row to supported, since this is the change that makes it true. <details> <summary>Implementation details</summary> ```python # 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() ``` **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** [#55](#55), and the matching Java change in [openfeature-java-server#61](launchdarkly/openfeature-java-server#61). Related spec change: [sdk-specs#257](launchdarkly/sdk-specs#257). **Describe the solution you've provided** `main` has since merged [#56](#56), [#57](#57) and [#58](#58); those are merged into this branch, so `start_wait` composes with the wrapper information now passed to `LDClient`, and a failed initialization raises `ProviderNotReadyError` rather than a fatal error. **Describe alternatives you've considered** Waiting on the ready event for `start_wait` seconds inside `initialize` as well: that doubled the effective wait, since the client constructor had already waited the same amount. **Additional context** Testing: `make test` (84 passed) and `make lint`. Tests cover the default matching the SDK default and initialization failing without waiting a second time; the timing assertion fails rather than hanging CI if the double wait comes back. </details> Link to Devin session: https://app.devin.ai/sessions/38a6eaf69fcf41109e136a1d0fe5e899 Open in Devin Desktop: https://app.devin.ai/desktop/session/38a6eaf69fcf41109e136a1d0fe5e899?variant=devin Requested by: @kinyoklion <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit 69390f7. 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>
1 parent 20ba8c1 commit c8708fd

4 files changed

Lines changed: 61 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Python](https
3939
|| Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
4040
|| 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. |
4141
|| Tracking | `track` sends a LaunchDarkly custom event for the evaluation context, with the tracking event value and remaining details attached. |
42-
| ⚠️ | 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). |
42+
| | 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. |
4343
|| Shutdown | `shutdown` closes the LaunchDarkly client; a closed client cannot be restarted, so a new provider instance is required afterward. |
4444
|| 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. |
4545
|| Extending | The underlying LaunchDarkly client is available through the `client` property. |
@@ -70,6 +70,8 @@ api.set_provider(openfeature_provider)
7070
# Refer to OpenFeature documentation for getting a client and performing evaluations.
7171
```
7272

73+
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.
74+
7375
Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK.
7476

7577
For information on using the OpenFeature client please refer to the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/).

ld_openfeature/provider.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,20 @@
2626

2727

2828
class LaunchDarklyProvider(AbstractProvider):
29-
def __init__(self, config: Config):
30-
self.__client = LDClient(config.with_wrapper_information(WRAPPER_NAME, VERSION))
29+
def __init__(self, config: Config, start_wait: float = 5):
30+
"""
31+
Create a provider backed by a LaunchDarkly client.
32+
33+
:param config: The LaunchDarkly client configuration.
34+
:param start_wait: The number of seconds to wait for a successful connection to LaunchDarkly, matching
35+
the same parameter of :class:`ldclient.LDClient`. A positive value bounds the whole of initialization:
36+
this constructor blocks for up to that long, and ``initialize`` then completes immediately, reporting
37+
a failed initialization if the client did not become ready in time. Zero does not block this
38+
constructor at all, and ``initialize`` then waits without a deadline for the data source to become
39+
valid or to fail permanently.
40+
"""
41+
self.__client = LDClient(config.with_wrapper_information(WRAPPER_NAME, VERSION), start_wait)
42+
self.__start_wait = start_wait
3143

3244
self.__context_converter = EvaluationContextConverter()
3345
self.__details_converter = ResolutionDetailsConverter()
@@ -84,7 +96,9 @@ def ready_handler(status: DataSourceStatus):
8496
if self.__client.is_initialized():
8597
ready_event.set()
8698

87-
ready_event.wait()
99+
# With a start wait the client constructor has already waited, so the outcome is whatever it is now.
100+
if self.__start_wait <= 0:
101+
ready_event.wait()
88102

89103
self.__client.data_source_status_provider.remove_listener(ready_handler)
90104

tests/test_data_sources.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ def initialized(self):
4040
return False
4141

4242

43+
class NeverReadyDataSource(UpdateProcessor):
44+
def __init__(self, config: Config, store, ready: threading.Event):
45+
self._ready = ready
46+
47+
def start(self):
48+
pass
49+
50+
def stop(self):
51+
pass
52+
53+
def is_alive(self):
54+
return False
55+
56+
def initialized(self):
57+
return False
58+
59+
4360
class DelayedFailingDataSource(UpdateProcessor):
4461
def __init__(self, config: Config, store, ready: threading.Event):
4562
self._data_source_update_sink: Optional[DataSourceUpdateSink] = config.data_source_update_sink

tests/test_provider.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from openfeature import api
1717

1818
from ld_openfeature import LaunchDarklyProvider, Config
19+
from tests.test_data_sources import FailingDataSource, InitializedThenFailingDataSource, NeverReadyDataSource, StaleDataSource, UpdatingDataSource, DelayedFailingDataSource
1920
from ld_openfeature.version import VERSION
20-
from tests.test_data_sources import FailingDataSource, InitializedThenFailingDataSource, StaleDataSource, UpdatingDataSource, DelayedFailingDataSource
2121

2222

2323
@pytest.fixture
@@ -50,6 +50,29 @@ def test_ldclient_is_accessible(provider: LaunchDarklyProvider):
5050
assert type(provider.client) is LDClient
5151

5252

53+
def test_default_start_wait_matches_launchdarkly_sdk_default():
54+
config = Config("", offline=True)
55+
56+
with patch("ld_openfeature.provider.LDClient") as client:
57+
LaunchDarklyProvider(config)
58+
59+
assert client.call_args.args[1] == 5
60+
61+
62+
def test_initialization_fails_without_waiting_again_with_positive_start_wait():
63+
provider = LaunchDarklyProvider(
64+
Config("", update_processor_class=NeverReadyDataSource, send_events=False),
65+
start_wait=0.5,
66+
)
67+
68+
started = time.time()
69+
with pytest.raises(ProviderNotReadyError):
70+
provider.initialize(EvaluationContext("user-key"))
71+
72+
assert time.time() - started < 0.25
73+
provider.shutdown()
74+
75+
5376
def test_provider_identifies_itself_as_the_wrapper(provider: LaunchDarklyProvider, config: Config):
5477
assert provider.client._config.wrapper_name == "open-feature-python-server"
5578
assert provider.client._config.wrapper_version == VERSION

0 commit comments

Comments
 (0)