Skip to content

Commit 45f7fd1

Browse files
fix: Improved handling within ProviderRegistry (#560)
* fix: Validate domain is present when calling `set_provider` on registry It is incorrect to call `ProviderRegister.set_provider` without a provider AND a domain. A validation check exists for the provider, but none for the domain. In this commit, we introduce that domain validation and introduce tests to capture this expected behavior. Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> * fix: Prevent providers from being initialized multiple times If a provider is set for two different domains, it will only be initialized once. However, if a customer were to use a provider as both default and domain specific, it would incorrectly be initialized twice. Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> * fix: Prevent providers from being shutdown multiple times If a provider is set for two different domains, and then one of them is replaced, it will only be shutdown once. However, if a customer were to use a provider as both default and domain specific, replacing one or the other would incorrectly shutdown both usages. Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> * refactor: Delete provider status instead of marking as NOT_READY Once a provider has been removed, there is no value in retaining a reference to it in the `_provider_status` dictionary. Instead of explicitly setting the status as `NOT_READY`, we can rely on this being the default status served from the `get_provider_status` method, allowing us to remove the dictionary entry entirely. Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> * Swap order for faster comparison Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> * formatting Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> --------- Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
1 parent 71ebb9f commit 45f7fd1

File tree

2 files changed

+136
-5
lines changed

2 files changed

+136
-5
lines changed

openfeature/provider/_registry.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ def set_provider(self, domain: str, provider: FeatureProvider) -> None:
3030
if domain in providers:
3131
old_provider = providers[domain]
3232
del providers[domain]
33-
if old_provider not in providers.values():
33+
if (
34+
old_provider != self._default_provider
35+
and old_provider not in providers.values()
36+
):
3437
self._shutdown_provider(old_provider)
35-
if provider not in providers.values():
38+
if provider != self._default_provider and provider not in providers.values():
3639
self._initialize_provider(provider)
3740
providers[domain] = provider
3841

@@ -44,10 +47,15 @@ def get_provider(self, domain: str | None) -> FeatureProvider:
4447
def set_default_provider(self, provider: FeatureProvider) -> None:
4548
if provider is None:
4649
raise GeneralError(error_message="No provider")
47-
if self._default_provider:
50+
if (
51+
self._default_provider
52+
and self._default_provider not in self._providers.values()
53+
):
4854
self._shutdown_provider(self._default_provider)
4955
self._default_provider = provider
50-
self._initialize_provider(provider)
56+
57+
if self._default_provider not in self._providers.values():
58+
self._initialize_provider(provider)
5159

5260
def get_default_provider(self) -> FeatureProvider:
5361
return self._default_provider
@@ -94,7 +102,7 @@ def _shutdown_provider(self, provider: FeatureProvider) -> None:
94102
try:
95103
if hasattr(provider, "shutdown"):
96104
provider.shutdown()
97-
self._provider_status[provider] = ProviderStatus.NOT_READY
105+
del self._provider_status[provider]
98106
except Exception as err:
99107
self.dispatch_event(
100108
provider,

tests/provider/test_registry.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from openfeature.exception import GeneralError
6+
from openfeature.provider import ProviderStatus
67
from openfeature.provider._registry import ProviderRegistry
78
from openfeature.provider.no_op_provider import NoOpProvider
89

@@ -105,3 +106,125 @@ def test_setting_default_provider_initializes_it():
105106
registry.set_default_provider(provider)
106107

107108
provider.initialize.assert_called_once()
109+
110+
111+
def test_registering_provider_as_default_then_domain_only_initializes_once():
112+
"""Test that registering the same provider as default and for a domain only initializes it once."""
113+
114+
registry = ProviderRegistry()
115+
provider = Mock()
116+
117+
registry.set_default_provider(provider)
118+
registry.set_provider("domain", provider)
119+
120+
provider.initialize.assert_called_once()
121+
122+
123+
def test_registering_provider_as_domain_then_default_only_initializes_once():
124+
"""Test that registering the same provider as default and for a domain only initializes it once."""
125+
126+
registry = ProviderRegistry()
127+
provider = Mock()
128+
129+
registry.set_provider("domain", provider)
130+
registry.set_default_provider(provider)
131+
132+
provider.initialize.assert_called_once()
133+
134+
135+
def test_replacing_provider_used_as_default_does_not_shutdown():
136+
"""Test that replacing a provider that is also the default does not shut it down twice."""
137+
138+
registry = ProviderRegistry()
139+
provider1 = Mock()
140+
provider2 = Mock()
141+
142+
registry.set_default_provider(provider1)
143+
registry.set_provider("domain", provider1)
144+
145+
registry.set_provider("domain", provider2)
146+
147+
provider1.shutdown.assert_not_called()
148+
provider2.shutdown.assert_not_called()
149+
150+
151+
def test_replacing_default_provider_used_as_domain_does_not_shutdown():
152+
"""Test that replacing a default provider that is also used for a domain does not shut it down twice."""
153+
154+
registry = ProviderRegistry()
155+
provider1 = Mock()
156+
provider2 = Mock()
157+
158+
registry.set_provider("domain", provider1)
159+
registry.set_default_provider(provider1)
160+
161+
registry.set_default_provider(provider2)
162+
163+
provider1.shutdown.assert_not_called()
164+
provider2.shutdown.assert_not_called()
165+
166+
167+
def test_shutting_down_registry_shuts_down_providers_once():
168+
"""Test that shutting down the registry shuts down each provider only once."""
169+
170+
registry = ProviderRegistry()
171+
provider1 = Mock()
172+
provider2 = Mock()
173+
174+
registry.set_default_provider(provider1)
175+
registry.set_provider("domain1", provider1)
176+
177+
registry.set_provider("domain2a", provider2)
178+
registry.set_provider("domain2b", provider2)
179+
180+
registry.shutdown()
181+
182+
provider1.shutdown.assert_called_once()
183+
provider2.shutdown.assert_called_once()
184+
185+
186+
def test_initializing_provider_sets_status_ready():
187+
"""Test that initializing a provider sets its status to READY."""
188+
189+
registry = ProviderRegistry()
190+
provider = Mock()
191+
192+
assert registry.get_provider_status(provider) == ProviderStatus.NOT_READY
193+
194+
registry.set_provider("domain", provider)
195+
196+
provider.initialize.assert_called_once()
197+
assert registry.get_provider_status(provider) == ProviderStatus.READY
198+
199+
200+
def test_shutting_down_provider_sets_status_not_ready():
201+
"""Test that shutting down a provider sets its status to NOT_READY."""
202+
203+
registry = ProviderRegistry()
204+
provider = Mock()
205+
206+
registry.set_provider("domain", provider)
207+
assert registry.get_provider_status(provider) == ProviderStatus.READY
208+
209+
registry.shutdown()
210+
assert registry.get_provider_status(provider) == ProviderStatus.NOT_READY
211+
212+
213+
def test_clearing_registry_resets_providers_and_default():
214+
"""Test that clearing the registry resets all providers and the default provider."""
215+
216+
registry = ProviderRegistry()
217+
provider = Mock()
218+
219+
registry.set_provider("domain", provider)
220+
registry.set_default_provider(provider)
221+
222+
registry.clear_providers()
223+
224+
default_provider = registry.get_default_provider()
225+
assert isinstance(default_provider, NoOpProvider)
226+
assert registry.get_provider("domain") is default_provider
227+
assert registry.get_provider_status(default_provider) == ProviderStatus.READY
228+
229+
provider.initialize.assert_called_once()
230+
provider.shutdown.assert_called_once()

0 commit comments

Comments
 (0)