99
1010from ldclient .async_config import AsyncConfig
1111from ldclient .impl .aio .concurrency import AsyncEvent , AsyncRepeatingTask
12- from ldclient .impl .datasource .datasource_common import sink_or_store
12+ from ldclient .impl .datasource .datasource_common import async_sink_or_store
13+ from ldclient .impl .retry import (
14+ FailureKind ,
15+ RetryState ,
16+ classify_http_status ,
17+ for_polling
18+ )
1319from ldclient .impl .util import (
1420 UnsuccessfulResponseException ,
15- http_error_message ,
16- is_http_error_recoverable ,
21+ http_error_description ,
1722 log
1823)
1924from ldclient .interfaces import (
2732
2833
2934class AsyncPollingUpdateProcessor (AsyncUpdateProcessor ):
30- def __init__ (self , config : AsyncConfig , requester : AsyncFeatureRequester , store : AsyncFeatureStore , ready : AsyncEvent ):
35+ """Polls LaunchDarkly for flag data on its own background task.
36+
37+ The loop reads its wait from the retry state, which ``_fetch_and_store``
38+ updates, so a failure can push the next poll further out than the poll
39+ interval. See :mod:`ldclient.impl.retry`.
40+ """
41+
42+ def __init__ (self , config : AsyncConfig , requester : AsyncFeatureRequester , store : AsyncFeatureStore , ready : AsyncEvent , retry_state : Optional [RetryState ] = None ):
3143 self ._config = config
3244 self ._data_source_update_sink = config .data_source_update_sink
3345 self ._requester = requester
3446 self ._store = store
3547 self ._ready = ready
36- self ._task = AsyncRepeatingTask .at_interval ("ldclient.datasource.polling" , config .poll_interval , 0 , self ._fetch_and_store )
48+ self ._retry = retry_state or for_polling (config .poll_interval )
49+ # No initial delay: the first poll is immediate.
50+ self ._task = AsyncRepeatingTask ("ldclient.datasource.polling" , self ._retry , 0 , self ._fetch_and_store )
3751
3852 def start (self ):
3953 log .info ("Starting AsyncPollingUpdateProcessor with request interval: " + str (self ._config .poll_interval ))
@@ -43,48 +57,53 @@ def initialized(self):
4357 return self ._ready .is_set () and self ._store .initialized
4458
4559 async def stop (self ):
46- self .__stop_with_error_info (None )
47- # Wait for the current poll to finish before closing the transport, so we do
48- # not close it while a request is still using it. The close is in a finally
49- # so an owned transport is still released if stop() is cancelled mid-wait.
50- try :
51- await self ._task .wait_stopped ()
52- finally :
53- await self ._requester .close ()
54-
55- def __stop_with_error_info (self , error : Optional [DataSourceErrorInfo ]):
5660 log .info ("Stopping AsyncPollingUpdateProcessor" )
5761 self ._task .stop ()
5862
59- if self ._data_source_update_sink is None :
60- return
63+ if self ._data_source_update_sink is not None :
64+ self . _data_source_update_sink . update_status ( DataSourceState . OFF , None )
6165
62- self ._data_source_update_sink .update_status (DataSourceState .OFF , error )
66+ # Do not close the transport while an in-flight request still uses it.
67+ try :
68+ await self ._task .wait_stopped ()
69+ finally :
70+ await self ._requester .close ()
6371
64- async def _fetch_and_store (self ):
72+ async def _fetch_and_store (self ) -> None :
73+ """Makes one poll request and records the outcome on the retry state."""
6574 try :
6675 all_data = await self ._requester .get_all_data ()
67- await sink_or_store (self ._data_source_update_sink , self ._store ).init (all_data )
76+ await async_sink_or_store (self ._data_source_update_sink , self ._store ).init (all_data )
77+
78+ if self ._data_source_update_sink is not None :
79+ self ._data_source_update_sink .update_status (DataSourceState .VALID , None )
80+
81+ # Report the status before signaling readiness, so a caller that
82+ # wakes on readiness cannot still read INITIALIZING.
6883 if not self ._ready .is_set () and self ._store .initialized :
6984 log .info ("AsyncPollingUpdateProcessor initialized ok" )
7085 self ._ready .set ()
7186
72- if self ._data_source_update_sink is not None :
73- self . _data_source_update_sink . update_status ( DataSourceState . VALID , None )
87+ self ._retry . record_success ()
88+ return
7489 except UnsuccessfulResponseException as e :
90+ kind = classify_http_status (e .status )
7591 error_info = DataSourceErrorInfo (DataSourceErrorKind .ERROR_RESPONSE , e .status , time .time (), str (e ))
92+ description = "Received %s for polling request" % http_error_description (e .status )
93+ level = log .error if kind is FailureKind .UNEXPECTED else log .warning
94+ stacktrace = None
95+ except Exception as e :
96+ kind = FailureKind .NORMAL
97+ error_info = DataSourceErrorInfo (DataSourceErrorKind .UNKNOWN , 0 , time .time (), str (e ))
98+ description = "Error encountered when updating flags: %s" % e
99+ level = log .error
100+ # The exception is passed explicitly: by the time the message is
101+ # logged, the handler has exited and exc_info() is empty.
102+ stacktrace = e
76103
77- http_error_message_result = http_error_message (e .status , "polling request" )
78- if not is_http_error_recoverable (e .status ):
79- log .error (http_error_message_result )
80- self ._ready .set () # if client is initializing, make it stop waiting; has no effect if already inited
81- self .__stop_with_error_info (error_info )
82- else :
83- log .warning (http_error_message_result )
104+ self ._retry .record_failure (kind )
105+ delay = self ._retry .next_delay
106+ level ("%s - will retry in %.1fs" % (description , delay ), exc_info = stacktrace )
84107
85- if self ._data_source_update_sink is not None :
86- self ._data_source_update_sink .update_status (DataSourceState .INTERRUPTED , error_info )
87- except Exception as e :
88- log .exception ('Error: Exception encountered when updating flags. %s' % e )
89- if self ._data_source_update_sink is not None :
90- self ._data_source_update_sink .update_status (DataSourceState .INTERRUPTED , DataSourceErrorInfo (DataSourceErrorKind .UNKNOWN , 0 , time .time (), str (e )))
108+ if self ._data_source_update_sink is not None :
109+ self ._data_source_update_sink .update_status (DataSourceState .INTERRUPTED , error_info )
0 commit comments