Skip to content

Commit cd994d6

Browse files
feat!: make set_provider non-blocking, add set_provider_and_wait
1 parent 02cd52c commit cd994d6

8 files changed

Lines changed: 229 additions & 30 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ api.set_provider(NoOpProvider())
130130
open_feature_client = api.get_client()
131131
```
132132

133+
`set_provider()` is non-blocking: it registers the provider immediately and runs initialization in a background thread.
134+
Flag evaluations during the initialization window return the default value with a `PROVIDER_NOT_READY` error code.
135+
Use `set_provider_and_wait()` if you need to ensure the provider is ready before proceeding:
136+
137+
```python
138+
# blocks until the provider is initialized (or raises on failure)
139+
api.set_provider_and_wait(NoOpProvider())
140+
```
141+
133142
In some situations, it may be beneficial to register multiple providers in the same application.
134143
This is possible using [domains](#domains), which is covered in more detail below.
135144

openfeature/api.py

Lines changed: 10 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",
@@ -50,6 +51,15 @@ def set_provider(provider: FeatureProvider, domain: str | None = None) -> None:
5051
provider_registry.set_provider(domain, provider)
5152

5253

54+
def set_provider_and_wait(
55+
provider: FeatureProvider, domain: str | None = None
56+
) -> None:
57+
if domain is None:
58+
provider_registry.set_default_provider(provider, wait_for_init=True)
59+
else:
60+
provider_registry.set_provider(domain, provider, wait_for_init=True)
61+
62+
5363
def clear_providers() -> None:
5464
provider_registry.clear_providers()
5565
_event_support.clear()

openfeature/provider/_registry.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import threading
2+
13
from openfeature._event_support import run_handlers_for_provider
24
from openfeature.evaluation_context import EvaluationContext, get_evaluation_context
35
from openfeature.event import (
@@ -21,7 +23,9 @@ def __init__(self) -> None:
2123
self._default_provider: ProviderStatus.READY,
2224
}
2325

24-
def set_provider(self, domain: str, provider: FeatureProvider) -> None:
26+
def set_provider(
27+
self, domain: str, provider: FeatureProvider, wait_for_init: bool = False
28+
) -> None:
2529
if provider is None:
2630
raise GeneralError(error_message="No provider")
2731
if domain is None:
@@ -36,15 +40,17 @@ def set_provider(self, domain: str, provider: FeatureProvider) -> None:
3640
):
3741
self._shutdown_provider(old_provider)
3842
if provider != self._default_provider and provider not in providers.values():
39-
self._initialize_provider(provider)
43+
self._initialize_provider(provider, wait_for_init=wait_for_init)
4044
providers[domain] = provider
4145

4246
def get_provider(self, domain: str | None) -> FeatureProvider:
4347
if domain is None:
4448
return self._default_provider
4549
return self._providers.get(domain, self._default_provider)
4650

47-
def set_default_provider(self, provider: FeatureProvider) -> None:
51+
def set_default_provider(
52+
self, provider: FeatureProvider, wait_for_init: bool = False
53+
) -> None:
4854
if provider is None:
4955
raise GeneralError(error_message="No provider")
5056
if (
@@ -55,7 +61,7 @@ def set_default_provider(self, provider: FeatureProvider) -> None:
5561
self._default_provider = provider
5662

5763
if self._default_provider not in self._providers.values():
58-
self._initialize_provider(provider)
64+
self._initialize_provider(provider, wait_for_init=wait_for_init)
5965

6066
def get_default_provider(self) -> FeatureProvider:
6167
return self._default_provider
@@ -75,8 +81,24 @@ def shutdown(self) -> None:
7581
def _get_evaluation_context(self) -> EvaluationContext:
7682
return get_evaluation_context()
7783

78-
def _initialize_provider(self, provider: FeatureProvider) -> None:
84+
def _initialize_provider(
85+
self, provider: FeatureProvider, wait_for_init: bool = False
86+
) -> None:
7987
provider.attach(self.dispatch_event)
88+
if wait_for_init:
89+
self._run_initialize(provider, raise_on_error=True)
90+
else:
91+
thread = threading.Thread(
92+
target=self._run_initialize,
93+
args=(provider,),
94+
kwargs={"raise_on_error": False},
95+
daemon=True,
96+
)
97+
thread.start()
98+
99+
def _run_initialize(
100+
self, provider: FeatureProvider, raise_on_error: bool = False
101+
) -> None:
80102
try:
81103
if hasattr(provider, "initialize"):
82104
provider.initialize(self._get_evaluation_context())
@@ -97,6 +119,8 @@ def _initialize_provider(self, provider: FeatureProvider) -> None:
97119
error_code=error_code,
98120
),
99121
)
122+
if raise_on_error:
123+
raise
100124

101125
def _shutdown_provider(self, provider: FeatureProvider) -> None:
102126
try:

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ def clear_providers():
1515

1616
@pytest.fixture()
1717
def no_op_provider_client():
18-
api.set_provider(NoOpProvider())
18+
api.set_provider_and_wait(NoOpProvider())
1919
return api.get_client()

tests/features/steps/metadata_steps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from behave import given, then
22

3-
from openfeature.api import get_client, set_provider
3+
from openfeature.api import get_client, set_provider_and_wait
44
from openfeature.provider.in_memory_provider import InMemoryProvider
55
from tests.features.data import IN_MEMORY_FLAGS
66

77

88
@given("a stable provider")
99
def step_impl_stable_provider(context):
10-
set_provider(InMemoryProvider(IN_MEMORY_FLAGS))
10+
set_provider_and_wait(InMemoryProvider(IN_MEMORY_FLAGS))
1111
context.client = get_client()
1212

1313

tests/features/steps/steps.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from behave import given, then, when
66

7-
from openfeature.api import get_client, set_provider
7+
from openfeature.api import get_client, set_provider_and_wait
88
from openfeature.client import OpenFeatureClient
99
from openfeature.evaluation_context import EvaluationContext
1010
from openfeature.exception import ErrorCode
@@ -28,13 +28,13 @@ def step_impl_resolved_should_be(context, flag_type, key, expected_reason):
2828

2929
@given("a provider is registered with cache disabled")
3030
def step_impl_provider_without_cache(context):
31-
set_provider(InMemoryProvider(IN_MEMORY_FLAGS))
31+
set_provider_and_wait(InMemoryProvider(IN_MEMORY_FLAGS))
3232
context.client = get_client()
3333

3434

3535
@given("a provider is registered")
3636
def step_impl_provider(context):
37-
set_provider(InMemoryProvider(IN_MEMORY_FLAGS))
37+
set_provider_and_wait(InMemoryProvider(IN_MEMORY_FLAGS))
3838
context.client = get_client()
3939

4040

tests/provider/test_registry.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import threading
12
from unittest.mock import Mock
23

34
import pytest
45

5-
from openfeature.exception import GeneralError
6+
from openfeature.exception import GeneralError, ProviderFatalError
67
from openfeature.provider import ProviderStatus
78
from openfeature.provider._registry import ProviderRegistry
89
from openfeature.provider.no_op_provider import NoOpProvider
@@ -67,8 +68,8 @@ def test_registering_provider_for_first_time_initializes_it():
6768
registry = ProviderRegistry()
6869
provider = Mock()
6970

70-
registry.set_provider("domain1", provider)
71-
registry.set_provider("domain2", provider)
71+
registry.set_provider("domain1", provider, wait_for_init=True)
72+
registry.set_provider("domain2", provider, wait_for_init=True)
7273

7374
provider.initialize.assert_called_once()
7475

@@ -103,7 +104,7 @@ def test_setting_default_provider_initializes_it():
103104
registry = ProviderRegistry()
104105
provider = Mock()
105106

106-
registry.set_default_provider(provider)
107+
registry.set_default_provider(provider, wait_for_init=True)
107108

108109
provider.initialize.assert_called_once()
109110

@@ -114,8 +115,8 @@ def test_registering_provider_as_default_then_domain_only_initializes_once():
114115
registry = ProviderRegistry()
115116
provider = Mock()
116117

117-
registry.set_default_provider(provider)
118-
registry.set_provider("domain", provider)
118+
registry.set_default_provider(provider, wait_for_init=True)
119+
registry.set_provider("domain", provider, wait_for_init=True)
119120

120121
provider.initialize.assert_called_once()
121122

@@ -126,8 +127,8 @@ def test_registering_provider_as_domain_then_default_only_initializes_once():
126127
registry = ProviderRegistry()
127128
provider = Mock()
128129

129-
registry.set_provider("domain", provider)
130-
registry.set_default_provider(provider)
130+
registry.set_provider("domain", provider, wait_for_init=True)
131+
registry.set_default_provider(provider, wait_for_init=True)
131132

132133
provider.initialize.assert_called_once()
133134

@@ -191,7 +192,7 @@ def test_initializing_provider_sets_status_ready():
191192

192193
assert registry.get_provider_status(provider) == ProviderStatus.NOT_READY
193194

194-
registry.set_provider("domain", provider)
195+
registry.set_provider("domain", provider, wait_for_init=True)
195196

196197
provider.initialize.assert_called_once()
197198
assert registry.get_provider_status(provider) == ProviderStatus.READY
@@ -203,7 +204,7 @@ def test_shutting_down_provider_sets_status_not_ready():
203204
registry = ProviderRegistry()
204205
provider = Mock()
205206

206-
registry.set_provider("domain", provider)
207+
registry.set_provider("domain", provider, wait_for_init=True)
207208
assert registry.get_provider_status(provider) == ProviderStatus.READY
208209

209210
registry.shutdown()
@@ -216,8 +217,8 @@ def test_clearing_registry_resets_providers_and_default():
216217
registry = ProviderRegistry()
217218
provider = Mock()
218219

219-
registry.set_provider("domain", provider)
220-
registry.set_default_provider(provider)
220+
registry.set_provider("domain", provider, wait_for_init=True)
221+
registry.set_default_provider(provider, wait_for_init=True)
221222

222223
registry.clear_providers()
223224

@@ -228,3 +229,53 @@ def test_clearing_registry_resets_providers_and_default():
228229

229230
provider.initialize.assert_called_once()
230231
provider.shutdown.assert_called_once()
232+
233+
234+
def test_set_provider_returns_before_initialization_completes():
235+
"""Test that set_provider (non-blocking) returns before initialize finishes."""
236+
237+
registry = ProviderRegistry()
238+
init_started = threading.Event()
239+
init_may_proceed = threading.Event()
240+
provider = Mock()
241+
242+
def slow_initialize(ctx):
243+
init_started.set()
244+
init_may_proceed.wait()
245+
246+
provider.initialize.side_effect = slow_initialize
247+
248+
registry.set_provider("domain", provider)
249+
250+
assert init_started.wait(timeout=2), "initialize was never called in background"
251+
assert registry.get_provider_status(provider) == ProviderStatus.NOT_READY
252+
253+
init_may_proceed.set() # unblock the background thread
254+
255+
256+
def test_set_provider_and_wait_blocks_until_ready():
257+
"""Test that set_provider with wait_for_init=True blocks until READY."""
258+
259+
registry = ProviderRegistry()
260+
initialized = threading.Event()
261+
provider = Mock()
262+
263+
def tracking_initialize(ctx):
264+
initialized.set()
265+
266+
provider.initialize.side_effect = tracking_initialize
267+
268+
registry.set_provider("domain", provider, wait_for_init=True)
269+
270+
assert initialized.is_set()
271+
assert registry.get_provider_status(provider) == ProviderStatus.READY
272+
273+
274+
def test_set_provider_and_wait_reraises_on_error():
275+
"""Test that set_provider with wait_for_init=True re-raises initialization errors."""
276+
registry = ProviderRegistry()
277+
provider = Mock()
278+
provider.initialize.side_effect = ProviderFatalError()
279+
280+
with pytest.raises(ProviderFatalError):
281+
registry.set_provider("domain", provider, wait_for_init=True)

0 commit comments

Comments
 (0)