@@ -127,11 +127,11 @@ async def variation_eval_fn(key, context):
127127 )
128128
129129 async def start (self , start_wait : float = 5.0 ) -> None :
130- """Start the client: create the HTTP session, data system, and event processor.
130+ """Start the client's background work: create the shared HTTP session and
131+ start the data source, the big-segment status poll, and the event processor.
131132
132- Single-shot. Calling start() again after it has started is a no-op. If
133- start() raises, the client is spent: it is marked closed and later
134- start() calls are ignored, so construct a new client to retry. Calling
133+ Single-shot. Calling start() again after it has started -- or after a
134+ failed start -- is a no-op; construct a new client to retry. Calling
135135 start() after close() is also a logged no-op.
136136
137137 :param start_wait: seconds to wait for the data source to initialize
@@ -143,48 +143,33 @@ async def start(self, start_wait: float = 5.0) -> None:
143143 if self ._started :
144144 return
145145
146- # __start_up resets the hook list to config.hooks + plugin hooks;
147- # preserve any hooks registered via add_hook() before start().
148- pre_start_hooks = [h for h in self .__hooks if h not in self ._config .hooks ]
146+ self ._started = True
149147
150148 try :
151149 await self .__start_up (start_wait )
152- self ._started = True
153150 except BaseException :
154- # Catch BaseException, not Exception, so a cancelled start
155- # (CancelledError) also releases the components __start_up began.
156- # re-raise propagates the error/cancellation; it is not swallowed.
151+ # Catch BaseException so a cancelled start (CancelledError) also
152+ # releases the components __start_up began; re-raise propagates it.
157153 await self ._cleanup_partial_start ()
158- self ._closed = True
159154 raise
160155
161- for hook in pre_start_hooks :
162- self .add_hook (hook )
163-
164156 async def _cleanup_partial_start (self ):
165- """Release any resources that were partially created during a failed __start_up."""
166- try :
167- await self ._event_processor .stop ()
168- except Exception :
169- pass
170- try :
171- await self ._data_system .stop ()
172- except Exception :
173- pass
174- try :
175- manager = self .__big_segment_store_manager
176- except AttributeError :
177- manager = None
178- if manager is not None :
157+ """Release any resources that were partially created during a failed
158+ __start_up."""
159+ for name , stop in (
160+ ("data system" , self ._data_system .stop ),
161+ ("big-segment store manager" , self .__big_segment_store_manager .stop ),
162+ ("event processor" , self ._event_processor .stop ),
163+ ):
179164 try :
180- await manager . stop ()
181- except Exception :
182- pass
165+ await stop ()
166+ except Exception as e :
167+ log . warning ( "Error stopping %s during failed start: %s" , name , e )
183168 if self ._session is not None :
184169 try :
185170 await self ._session .close ()
186- except Exception :
187- pass
171+ except Exception as e :
172+ log . warning ( "Error closing HTTP session during failed start: %s" , e )
188173 self ._session = None
189174
190175 async def close (self , close_timeout : float = 2.0 ) -> None :
@@ -197,6 +182,7 @@ async def close(self, close_timeout: float = 2.0) -> None:
197182 return
198183 self ._closed = True
199184
185+ # A client that was never started has nothing running to release.
200186 if self ._started :
201187 try :
202188 await asyncio .wait_for (self ._close_components (), timeout = close_timeout )
@@ -215,15 +201,19 @@ async def _close_components(self):
215201 """Releases the threads and network connections used by the SDK
216202 components. The public :meth:`close` wraps this with a timeout."""
217203 log .info ("Closing LaunchDarkly client.." )
218- await self ._event_processor .stop ()
219204 await self ._data_system .stop ()
220205 await self .__big_segment_store_manager .stop ()
221206
207+ # The event processor is last because it may still be sending events that were generated by the other components.
208+ await self ._event_processor .stop ()
209+
222210 async def __start_up (self , start_wait : float ):
223211 environment_metadata = get_environment_metadata (self ._config , "python-server-sdk-async" )
224212 plugin_hooks = get_plugin_hooks (self ._config .plugins , environment_metadata )
225213
226- self .__hooks = self ._config .hooks + plugin_hooks
214+ # Append plugin hooks to those already registered (the config hooks plus
215+ # any added via add_hook() before start()), rather than overwriting.
216+ self .__hooks = self .__hooks + plugin_hooks
227217
228218 # Start the big-segment status poll now that a loop is running.
229219 self .__big_segment_store_manager .start ()
@@ -325,7 +315,7 @@ def track_migration_op(self, tracker: OpTracker):
325315 event = tracker .build ()
326316
327317 if isinstance (event , str ):
328- log .error ("error generting migration op event %s; no event will be emitted" , event )
318+ log .error ("error generating migration op event %s; no event will be emitted" , event )
329319 return
330320
331321 self ._send_event (event )
@@ -354,7 +344,7 @@ def track(self, event_name: str, context: Context, data: Optional[Any] = None, m
354344 def identify (self , context : Context ):
355345 """Reports details about an evaluation context.
356346
357- This method simply creates an analytics event containing the context properties, to
347+ This method simply creates an analytics event containing the context properties, so
358348 that LaunchDarkly will know about that context if it does not already.
359349
360350 Evaluating a flag, by calling :func:`variation()` or :func:`variation_detail()`, also
@@ -393,7 +383,7 @@ async def flush(self):
393383 Normally, batches of events are delivered in the background at intervals determined by the
394384 ``flush_interval`` property of :class:`ldclient.config.Config`. Calling ``flush()``
395385 schedules the next event delivery to be as soon as possible; however, the delivery still
396- happens asynchronously on a worker thread , so this method will return immediately.
386+ happens asynchronously in the background , so this method will return immediately.
397387 """
398388 if self ._config .offline :
399389 return
0 commit comments