@@ -62,7 +62,10 @@ def __init__(self, client: FBClient, initialization_timeout: float = 15.0):
6262 self ._initialization_timeout = initialization_timeout
6363 self ._context_converter = EvaluationContextConverter ()
6464 self ._details_converter = ResolutionDetailsConverter ()
65- self ._lifecycle_lock = threading .Lock ()
65+ # Listener registration and event emission can synchronously re-enter
66+ # provider lifecycle methods through user callbacks.
67+ self ._lifecycle_lock = threading .RLock ()
68+ self ._shutdown_started = False
6669 self ._flag_change_listener : _FlagChangeListener | None = None
6770 self ._status_listener_registered = False
6871
@@ -75,19 +78,23 @@ def initialize(self, evaluation_context: EvaluationContext) -> None:
7578 # Register before waiting so a client that connects just after an
7679 # initialization timeout can still publish its recovery to OpenFeature.
7780 self ._register_listeners ()
78- if not self ._client .initialize :
79- ready = self ._client .update_status_provider .wait_for_OKState (
80- self ._initialization_timeout
81- )
82- if not ready :
83- state = self ._client .update_status_provider .current_state
84- message = self ._state_message (state )
85- if state .state_type == StateType .OFF :
86- raise ProviderFatalError (message )
87- raise ProviderNotReadyError (message )
81+ with self ._lifecycle_lock :
82+ if self ._shutdown_started :
83+ raise ProviderFatalError ("FeatBit provider is shut down" )
84+
85+ ready = self ._client .update_status_provider .wait_for_OKState (
86+ self ._initialization_timeout
87+ )
88+ if not ready :
89+ state = self ._client .update_status_provider .current_state
90+ message = self ._state_message (state )
91+ if state .state_type == StateType .OFF :
92+ raise ProviderFatalError (message )
93+ raise ProviderNotReadyError (message )
8894
8995 def shutdown (self ) -> None :
9096 with self ._lifecycle_lock :
97+ self ._shutdown_started = True
9198 flag_listener = self ._flag_change_listener
9299 status_listener_registered = self ._status_listener_registered
93100
@@ -264,6 +271,9 @@ def _type_mismatch_details(default_value: Any) -> FlagResolutionDetails[Any]:
264271
265272 def _register_listeners (self ) -> None :
266273 with self ._lifecycle_lock :
274+ if self ._shutdown_started :
275+ return
276+
267277 if not self ._status_listener_registered :
268278 try :
269279 self ._client .update_status_provider .add_listener (
@@ -285,32 +295,33 @@ def _register_listeners(self) -> None:
285295
286296 def _emit_configuration_changed (self , flag_key : str ) -> None :
287297 try :
288- self .emit_provider_configuration_changed (
289- ProviderEventDetails (flags_changed = [flag_key ])
290- )
298+ with self ._lifecycle_lock :
299+ if self ._shutdown_started :
300+ return
301+ self .emit_provider_configuration_changed (
302+ ProviderEventDetails (flags_changed = [flag_key ])
303+ )
291304 except Exception :
292305 logger .exception ("Failed to emit OpenFeature configuration change event" )
293306
294307 def _handle_data_update_status (self , state : State ) -> None :
295308 try :
296- if state .state_type == StateType .OK :
297- self .emit_provider_ready (ProviderEventDetails ())
298- elif state .state_type == StateType .INTERRUPTED :
299- self .emit_provider_stale (
300- ProviderEventDetails (message = self ._state_message (state ))
301- )
302- elif state .state_type == StateType .OFF :
303- if state .error_track is not None :
309+ with self ._lifecycle_lock :
310+ if self ._shutdown_started :
311+ return
312+ if state .state_type == StateType .OK :
313+ self .emit_provider_ready (ProviderEventDetails ())
314+ elif state .state_type == StateType .INTERRUPTED :
315+ self .emit_provider_stale (
316+ ProviderEventDetails (message = self ._state_message (state ))
317+ )
318+ elif state .state_type == StateType .OFF :
304319 self .emit_provider_error (
305320 ProviderEventDetails (
306321 error_code = ErrorCode .PROVIDER_FATAL ,
307322 message = self ._state_message (state ),
308323 )
309324 )
310- # A normal OFF is produced by FBClient.stop(). Provider
311- # shutdown unregisters this listener before stopping the
312- # client; OpenFeature then removes the provider status, whose
313- # standard fallback is NOT_READY. Do not misreport it FATAL.
314325 except Exception :
315326 logger .exception ("Failed to emit OpenFeature provider status event" )
316327
0 commit comments