Skip to content

Commit 9966c14

Browse files
authored
feat: update provider status when provider emits events (#309)
* refactor: move registry singleton to the registry module Signed-off-by: Federico Bond <[email protected]> * refactor: make openfeature.provider.registry a private module Signed-off-by: Federico Bond <[email protected]> * feat: update provider status when provider emits events Signed-off-by: Federico Bond <[email protected]> * refactor: avoid duplicate code Signed-off-by: Federico Bond <[email protected]> * fix: fix provider event dispatch on initialize/shutdown Signed-off-by: Federico Bond <[email protected]> * refactor: rename default_registry to provider_registry Signed-off-by: Federico Bond <[email protected]> --------- Signed-off-by: Federico Bond <[email protected]>
1 parent faf02a9 commit 9966c14

File tree

5 files changed

+114
-33
lines changed

5 files changed

+114
-33
lines changed

openfeature/api.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@
1010
from openfeature.exception import GeneralError
1111
from openfeature.hook import Hook
1212
from openfeature.provider import FeatureProvider
13+
from openfeature.provider._registry import provider_registry
1314
from openfeature.provider.metadata import Metadata
14-
from openfeature.provider.registry import ProviderRegistry
1515

1616
_evaluation_context = EvaluationContext()
1717

1818
_hooks: typing.List[Hook] = []
1919

20-
_provider_registry: ProviderRegistry = ProviderRegistry()
21-
2220

2321
def get_client(
2422
domain: typing.Optional[str] = None, version: typing.Optional[str] = None
@@ -30,18 +28,18 @@ def set_provider(
3028
provider: FeatureProvider, domain: typing.Optional[str] = None
3129
) -> None:
3230
if domain is None:
33-
_provider_registry.set_default_provider(provider)
31+
provider_registry.set_default_provider(provider)
3432
else:
35-
_provider_registry.set_provider(domain, provider)
33+
provider_registry.set_provider(domain, provider)
3634

3735

3836
def clear_providers() -> None:
39-
_provider_registry.clear_providers()
37+
provider_registry.clear_providers()
4038
_event_support.clear()
4139

4240

4341
def get_provider_metadata(domain: typing.Optional[str] = None) -> Metadata:
44-
return _provider_registry.get_provider(domain).get_metadata()
42+
return provider_registry.get_provider(domain).get_metadata()
4543

4644

4745
def get_evaluation_context() -> EvaluationContext:
@@ -72,7 +70,7 @@ def get_hooks() -> typing.List[Hook]:
7270

7371

7472
def shutdown() -> None:
75-
_provider_registry.shutdown()
73+
provider_registry.shutdown()
7674

7775

7876
def add_handler(event: ProviderEvent, handler: EventHandler) -> None:

openfeature/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
error_hooks,
2929
)
3030
from openfeature.provider import FeatureProvider, ProviderStatus
31+
from openfeature.provider._registry import provider_registry
3132

3233
logger = logging.getLogger("openfeature")
3334

@@ -82,10 +83,10 @@ def __init__(
8283

8384
@property
8485
def provider(self) -> FeatureProvider:
85-
return api._provider_registry.get_provider(self.domain)
86+
return provider_registry.get_provider(self.domain)
8687

8788
def get_provider_status(self) -> ProviderStatus:
88-
return api._provider_registry.get_provider_status(self.provider)
89+
return provider_registry.get_provider_status(self.provider)
8990

9091
def get_metadata(self) -> ClientMetadata:
9192
return ClientMetadata(domain=self.domain)

openfeature/provider/registry.py renamed to openfeature/provider/_registry.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,31 +74,68 @@ def _initialize_provider(self, provider: FeatureProvider) -> None:
7474
try:
7575
if hasattr(provider, "initialize"):
7676
provider.initialize(self._get_evaluation_context())
77-
self._set_provider_status(provider, ProviderStatus.READY)
77+
self.dispatch_event(
78+
provider, ProviderEvent.PROVIDER_READY, ProviderEventDetails()
79+
)
7880
except Exception as err:
79-
if (
80-
isinstance(err, OpenFeatureError)
81-
and err.error_code == ErrorCode.PROVIDER_FATAL
82-
):
83-
self._set_provider_status(provider, ProviderStatus.FATAL)
84-
else:
85-
self._set_provider_status(provider, ProviderStatus.ERROR)
81+
error_code = (
82+
err.error_code
83+
if isinstance(err, OpenFeatureError)
84+
else ErrorCode.GENERAL
85+
)
86+
self.dispatch_event(
87+
provider,
88+
ProviderEvent.PROVIDER_ERROR,
89+
ProviderEventDetails(
90+
message=f"Provider initialization failed: {err}",
91+
error_code=error_code,
92+
),
93+
)
8694

8795
def _shutdown_provider(self, provider: FeatureProvider) -> None:
8896
try:
8997
if hasattr(provider, "shutdown"):
9098
provider.shutdown()
91-
self._set_provider_status(provider, ProviderStatus.NOT_READY)
92-
except Exception:
93-
self._set_provider_status(provider, ProviderStatus.FATAL)
99+
self._provider_status[provider] = ProviderStatus.NOT_READY
100+
except Exception as err:
101+
self.dispatch_event(
102+
provider,
103+
ProviderEvent.PROVIDER_ERROR,
104+
ProviderEventDetails(
105+
message=f"Provider shutdown failed: {err}",
106+
error_code=ErrorCode.PROVIDER_FATAL,
107+
),
108+
)
94109

95110
def get_provider_status(self, provider: FeatureProvider) -> ProviderStatus:
96111
return self._provider_status.get(provider, ProviderStatus.NOT_READY)
97112

98-
def _set_provider_status(
99-
self, provider: FeatureProvider, status: ProviderStatus
113+
def dispatch_event(
114+
self,
115+
provider: FeatureProvider,
116+
event: ProviderEvent,
117+
details: ProviderEventDetails,
100118
) -> None:
101-
self._provider_status[provider] = status
102-
103-
if event := ProviderEvent.from_provider_status(status):
104-
run_handlers_for_provider(provider, event, ProviderEventDetails())
119+
self._update_provider_status(provider, event, details)
120+
run_handlers_for_provider(provider, event, details)
121+
122+
def _update_provider_status(
123+
self,
124+
provider: FeatureProvider,
125+
event: ProviderEvent,
126+
details: ProviderEventDetails,
127+
) -> None:
128+
if event == ProviderEvent.PROVIDER_READY:
129+
self._provider_status[provider] = ProviderStatus.READY
130+
elif event == ProviderEvent.PROVIDER_STALE:
131+
self._provider_status[provider] = ProviderStatus.STALE
132+
elif event == ProviderEvent.PROVIDER_ERROR:
133+
status = (
134+
ProviderStatus.FATAL
135+
if details.error_code == ErrorCode.PROVIDER_FATAL
136+
else ProviderStatus.ERROR
137+
)
138+
self._provider_status[provider] = status
139+
140+
141+
provider_registry = ProviderRegistry()

openfeature/provider/provider.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import typing
22
from abc import abstractmethod
33

4-
from openfeature._event_support import run_handlers_for_provider
54
from openfeature.evaluation_context import EvaluationContext
65
from openfeature.event import ProviderEvent, ProviderEventDetails
76
from openfeature.flag_evaluation import FlagResolutionDetails
@@ -84,4 +83,6 @@ def emit_provider_stale(self, details: ProviderEventDetails) -> None:
8483
self.emit(ProviderEvent.PROVIDER_STALE, details)
8584

8685
def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None:
87-
run_handlers_for_provider(self, event, details)
86+
from openfeature.provider._registry import provider_registry
87+
88+
provider_registry.dispatch_event(self, event, details)

tests/test_api.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
)
1919
from openfeature.evaluation_context import EvaluationContext
2020
from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails
21-
from openfeature.exception import ErrorCode, GeneralError
21+
from openfeature.exception import ErrorCode, GeneralError, ProviderFatalError
2222
from openfeature.hook import Hook
23-
from openfeature.provider import FeatureProvider, Metadata
23+
from openfeature.provider import FeatureProvider, Metadata, ProviderStatus
2424
from openfeature.provider.no_op_provider import NoOpProvider
2525

2626

@@ -303,13 +303,57 @@ def test_handlers_attached_to_provider_already_in_associated_state_should_run_im
303303
def test_provider_ready_handlers_run_if_provider_initialize_function_terminates_normally():
304304
# Given
305305
provider = NoOpProvider()
306-
set_provider(provider)
307306

308307
spy = MagicMock()
309308
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
309+
spy.reset_mock() # reset the mock to avoid counting the immediate call on subscribe
310310

311311
# When
312-
provider.initialize(get_evaluation_context())
312+
set_provider(provider)
313313

314314
# Then
315315
spy.provider_ready.assert_called_once()
316+
317+
318+
def test_provider_error_handlers_run_if_provider_initialize_function_terminates_abnormally():
319+
# Given
320+
provider = MagicMock(spec=FeatureProvider)
321+
provider.initialize.side_effect = ProviderFatalError()
322+
323+
spy = MagicMock()
324+
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
325+
326+
# When
327+
set_provider(provider)
328+
329+
# Then
330+
spy.provider_error.assert_called_once()
331+
332+
333+
def test_provider_status_is_updated_after_provider_emits_event():
334+
# Given
335+
provider = NoOpProvider()
336+
set_provider(provider)
337+
client = get_client()
338+
339+
# When
340+
provider.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.GENERAL))
341+
# Then
342+
assert client.get_provider_status() == ProviderStatus.ERROR
343+
344+
# When
345+
provider.emit_provider_error(
346+
ProviderEventDetails(error_code=ErrorCode.PROVIDER_FATAL)
347+
)
348+
# Then
349+
assert client.get_provider_status() == ProviderStatus.FATAL
350+
351+
# When
352+
provider.emit_provider_stale(ProviderEventDetails())
353+
# Then
354+
assert client.get_provider_status() == ProviderStatus.STALE
355+
356+
# When
357+
provider.emit_provider_ready(ProviderEventDetails())
358+
# Then
359+
assert client.get_provider_status() == ProviderStatus.READY

0 commit comments

Comments
 (0)