Skip to content

Commit 8ddfef0

Browse files
committed
feat: add set_provider_and_wait() for blocking initialization
Adds set_provider_and_wait() to the public API, implementing spec requirement 1.1.2.4: "The API SHOULD provide functions to set a provider and wait for the initialize function to return or abnormally terminate." This is a purely additive change -- set_provider() behavior is unchanged. The only difference with set_provider_and_wait() is that initialization errors are re-raised to the caller. In a future major version, set_provider() should be changed to run initialize() in a background thread to match the non-blocking semantics of Java, Go, and Node.js SDKs. Signed-off-by: Leo Romanovsky <leo.romanovsky@datadoghq.com>
1 parent 05382aa commit 8ddfef0

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

openfeature/api.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"remove_handler",
3232
"set_evaluation_context",
3333
"set_provider",
34+
"set_provider_and_wait",
3435
"set_transaction_context",
3536
"set_transaction_context_propagator",
3637
"shutdown",
@@ -44,12 +45,36 @@ def get_client(
4445

4546

4647
def set_provider(provider: FeatureProvider, domain: str | None = None) -> None:
48+
"""Set the provider, calling initialize() synchronously.
49+
50+
Note: In a future major version, this function should run initialize()
51+
in a background thread to match the non-blocking semantics of
52+
set_provider() in the Java, Go, and Node.js SDKs. Callers who need
53+
blocking behavior should migrate to set_provider_and_wait().
54+
"""
4755
if domain is None:
4856
provider_registry.set_default_provider(provider)
4957
else:
5058
provider_registry.set_provider(domain, provider)
5159

5260

61+
def set_provider_and_wait(provider: FeatureProvider, domain: str | None = None) -> None:
62+
"""Set the provider and wait for initialization to complete.
63+
64+
Blocks the calling thread until the provider's initialize() method
65+
returns successfully or raises an exception. If initialization fails,
66+
the exception is re-raised to the caller.
67+
68+
Spec reference: Requirement 1.1.2.4 - "The API SHOULD provide functions
69+
to set a provider and wait for the initialize function to return or
70+
abnormally terminate."
71+
"""
72+
if domain is None:
73+
provider_registry.set_default_provider_and_wait(provider)
74+
else:
75+
provider_registry.set_provider_and_wait(domain, provider)
76+
77+
5378
def clear_providers() -> None:
5479
provider_registry.clear_providers()
5580
_event_support.clear()

openfeature/provider/_registry.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ def set_provider(self, domain: str, provider: FeatureProvider) -> None:
3939
self._initialize_provider(provider)
4040
providers[domain] = provider
4141

42+
def set_provider_and_wait(self, domain: str, provider: FeatureProvider) -> None:
43+
if provider is None:
44+
raise GeneralError(error_message="No provider")
45+
if domain is None:
46+
raise GeneralError(error_message="No domain")
47+
providers = self._providers
48+
if domain in providers:
49+
old_provider = providers[domain]
50+
del providers[domain]
51+
if (
52+
old_provider != self._default_provider
53+
and old_provider not in providers.values()
54+
):
55+
self._shutdown_provider(old_provider)
56+
if provider != self._default_provider and provider not in providers.values():
57+
self._initialize_provider_and_wait(provider)
58+
providers[domain] = provider
59+
4260
def get_provider(self, domain: str | None) -> FeatureProvider:
4361
if domain is None:
4462
return self._default_provider
@@ -57,6 +75,19 @@ def set_default_provider(self, provider: FeatureProvider) -> None:
5775
if self._default_provider not in self._providers.values():
5876
self._initialize_provider(provider)
5977

78+
def set_default_provider_and_wait(self, provider: FeatureProvider) -> None:
79+
if provider is None:
80+
raise GeneralError(error_message="No provider")
81+
if (
82+
self._default_provider
83+
and self._default_provider not in self._providers.values()
84+
):
85+
self._shutdown_provider(self._default_provider)
86+
self._default_provider = provider
87+
88+
if self._default_provider not in self._providers.values():
89+
self._initialize_provider_and_wait(provider)
90+
6091
def get_default_provider(self) -> FeatureProvider:
6192
return self._default_provider
6293

@@ -76,6 +107,39 @@ def _get_evaluation_context(self) -> EvaluationContext:
76107
return get_evaluation_context()
77108

78109
def _initialize_provider(self, provider: FeatureProvider) -> None:
110+
"""Initialize the provider synchronously. Errors are dispatched as
111+
PROVIDER_ERROR events but not re-raised to the caller.
112+
113+
This is the original behavior of set_provider().
114+
"""
115+
provider.attach(self.dispatch_event)
116+
try:
117+
if hasattr(provider, "initialize"):
118+
provider.initialize(self._get_evaluation_context())
119+
self.dispatch_event(
120+
provider, ProviderEvent.PROVIDER_READY, ProviderEventDetails()
121+
)
122+
except Exception as err:
123+
error_code = (
124+
err.error_code
125+
if isinstance(err, OpenFeatureError)
126+
else ErrorCode.GENERAL
127+
)
128+
self.dispatch_event(
129+
provider,
130+
ProviderEvent.PROVIDER_ERROR,
131+
ProviderEventDetails(
132+
message=f"Provider initialization failed: {err}",
133+
error_code=error_code,
134+
),
135+
)
136+
137+
def _initialize_provider_and_wait(self, provider: FeatureProvider) -> None:
138+
"""Initialize the provider synchronously and re-raise on failure.
139+
140+
Same as _initialize_provider but propagates exceptions to the caller,
141+
used by set_provider_and_wait() / set_default_provider_and_wait().
142+
"""
79143
provider.attach(self.dispatch_event)
80144
try:
81145
if hasattr(provider, "initialize"):
@@ -97,6 +161,7 @@ def _initialize_provider(self, provider: FeatureProvider) -> None:
97161
error_code=error_code,
98162
),
99163
)
164+
raise
100165

101166
def _shutdown_provider(self, provider: FeatureProvider) -> None:
102167
try:

tests/test_api.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
remove_handler,
1515
set_evaluation_context,
1616
set_provider,
17+
set_provider_and_wait,
1718
shutdown,
1819
)
1920
from openfeature.evaluation_context import EvaluationContext
@@ -330,6 +331,51 @@ def test_provider_error_handlers_run_if_provider_initialize_function_terminates_
330331
spy.provider_error.assert_called_once()
331332

332333

334+
def test_set_provider_and_wait_blocks_until_initialize_completes():
335+
# Given
336+
evaluation_context = EvaluationContext("targeting_key", {"attr1": "val1"})
337+
provider = MagicMock(spec=FeatureProvider)
338+
339+
# When
340+
set_evaluation_context(evaluation_context)
341+
set_provider_and_wait(provider)
342+
343+
# Then - initialize should have been called synchronously
344+
provider.initialize.assert_called_with(evaluation_context)
345+
# Provider should be READY after set_provider_and_wait returns
346+
client = get_client()
347+
assert client.get_provider_status() == ProviderStatus.READY
348+
349+
350+
def test_set_provider_and_wait_raises_on_initialization_failure():
351+
# Given
352+
provider = MagicMock(spec=FeatureProvider)
353+
provider.initialize.side_effect = ProviderFatalError()
354+
355+
spy = MagicMock()
356+
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
357+
358+
# When / Then - should propagate the exception to the caller
359+
with pytest.raises(ProviderFatalError):
360+
set_provider_and_wait(provider)
361+
362+
# Error handler should still have been called
363+
spy.provider_error.assert_called_once()
364+
365+
366+
def test_set_provider_and_wait_with_domain():
367+
# Given
368+
provider = MagicMock(spec=FeatureProvider)
369+
370+
# When
371+
set_provider_and_wait(provider, domain="test")
372+
373+
# Then
374+
provider.initialize.assert_called_once()
375+
test_client = get_client("test")
376+
assert test_client.provider == provider
377+
378+
333379
def test_provider_status_is_updated_after_provider_emits_event():
334380
# Given
335381
provider = NoOpProvider()

0 commit comments

Comments
 (0)