Skip to content

Commit 5b46606

Browse files
committed
fixup: pr feedback
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
1 parent 0ba9546 commit 5b46606

2 files changed

Lines changed: 43 additions & 16 deletions

File tree

openfeature/provider/_registry.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
if typing.TYPE_CHECKING:
1818
from openfeature._event_support import EventSupport
1919

20-
# spec 1.8.4: a provider should not be bound to more than one OpenFeature API
21-
# instance simultaneously. We track the owning registry per provider; rebinding
22-
# to a different registry raises. WeakKeyDictionary lets providers be GC'd.
20+
# spec 1.8.4: provider must not bind to more than one API; we track owning registry per provider, rebinding raises. WeakKeyDictionary lets providers be GC'd
2321
_binding_lock = threading.Lock()
2422
_provider_bindings: weakref.WeakKeyDictionary[FeatureProvider, ProviderRegistry] = (
2523
weakref.WeakKeyDictionary()
@@ -221,11 +219,8 @@ def _run_initialize(
221219
def _shutdown_if_unused(self, provider: FeatureProvider) -> None:
222220
# only shut down if no longer referenced. shutdown runs on a daemon
223221
# thread so a hanging shutdown() cannot block the caller.
224-
with self._lock:
225-
if provider is self._default_provider:
226-
return
227-
if provider in self._providers.values():
228-
return
222+
if self._is_active(provider):
223+
return
229224

230225
thread = threading.Thread(
231226
target=self._shutdown_provider,
@@ -235,20 +230,25 @@ def _shutdown_if_unused(self, provider: FeatureProvider) -> None:
235230
)
236231
thread.start()
237232

233+
def _is_active(self, provider: FeatureProvider) -> bool:
234+
with self._lock:
235+
return (
236+
provider is self._default_provider
237+
or provider in self._providers.values()
238+
)
239+
238240
def _shutdown_provider(
239241
self, provider: FeatureProvider, abort_if_re_registered: bool = False
240242
) -> None:
241243
try:
244+
# abort if re-registered before shutdown() to avoid tearing down the freshly-registered instance
245+
if abort_if_re_registered and self._is_active(provider):
246+
return
242247
if hasattr(provider, "shutdown"):
243248
provider.shutdown()
244-
# if provider is being re-registered, leave its status and event wiring intact
245-
if abort_if_re_registered:
246-
with self._lock:
247-
if (
248-
provider is self._default_provider
249-
or provider in self._providers.values()
250-
):
251-
return
249+
# abort if re-registered during shutdown(); leave status and event wiring intact
250+
if abort_if_re_registered and self._is_active(provider):
251+
return
252252
with self._lock:
253253
self._provider_status.pop(provider, None)
254254
except Exception as err:

tests/provider/test_registry.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,30 @@ def slow_shutdown():
433433
"stale shutdown of A clobbered the fresh registration's status"
434434
)
435435
provider_a.detach.assert_not_called()
436+
437+
438+
def test_stale_shutdown_skips_shutdown_if_re_registered_first():
439+
"""If a provider is re-registered before its background shutdown gets to
440+
call shutdown() at all, shutdown() must not be invoked on the active
441+
provider."""
442+
443+
registry = make_registry()
444+
445+
provider_a = Mock()
446+
provider_b = Mock()
447+
448+
# step 1: register A, replace with B, then re-register A. queued background shutdown of A from the A->B swap is racing
449+
registry.set_provider("domain", provider_a, wait_for_init=True)
450+
registry.set_provider("domain", provider_b, wait_for_init=True)
451+
registry.set_provider("domain", provider_a, wait_for_init=True)
452+
# let the natural A->B background shutdown complete before we assert
453+
time.sleep(0.2)
454+
provider_a.shutdown.reset_mock()
455+
provider_a.detach.reset_mock()
456+
457+
# step 2: simulate the late-arriving stale shutdown; abort check must short-circuit before shutdown() is called
458+
registry._shutdown_provider(provider_a, abort_if_re_registered=True)
459+
460+
provider_a.shutdown.assert_not_called()
461+
provider_a.detach.assert_not_called()
462+
assert registry.get_provider_status(provider_a) == ProviderStatus.READY

0 commit comments

Comments
 (0)