1818from ld_eventsource .errors import HTTPStatusError
1919
2020from ldclient .config import Config
21- from ldclient .impl .datasystem import SelectorStore , Synchronizer , Update
21+ from ldclient .impl .datasystem import (
22+ DiagnosticAccumulator ,
23+ DiagnosticSource ,
24+ SelectorStore ,
25+ Synchronizer ,
26+ Update
27+ )
2228from ldclient .impl .datasystem .protocolv2 import (
2329 ChangeSetBuilder ,
2430 DeleteObject ,
@@ -98,7 +104,7 @@ def query_params() -> dict[str, str]:
98104 )
99105
100106
101- class StreamingDataSource (Synchronizer ):
107+ class StreamingDataSource (Synchronizer , DiagnosticSource ):
102108 """
103109 StreamingSynchronizer is a specific type of Synchronizer that handles
104110 streaming data sources.
@@ -112,6 +118,11 @@ def __init__(self, config: Config):
112118 self ._config = config
113119 self ._sse : Optional [SSEClient ] = None
114120 self ._running = False
121+ self ._diagnostic_accumulator : Optional [DiagnosticAccumulator ] = None
122+ self ._connection_attempt_start_time : Optional [float ] = None
123+
124+ def set_diagnostic_accumulator (self , diagnostic_accumulator : DiagnosticAccumulator ):
125+ self ._diagnostic_accumulator = diagnostic_accumulator
115126
116127 @property
117128 def name (self ) -> str :
@@ -133,6 +144,7 @@ def sync(self, ss: SelectorStore) -> Generator[Update, None, None]:
133144
134145 change_set_builder = ChangeSetBuilder ()
135146 self ._running = True
147+ self ._connection_attempt_start_time = time ()
136148
137149 for action in self ._sse .all :
138150 if isinstance (action , Fault ):
@@ -153,6 +165,7 @@ def sync(self, ss: SelectorStore) -> Generator[Update, None, None]:
153165 if isinstance (action , Start ) and action .headers is not None :
154166 fallback = action .headers .get ('X-LD-FD-Fallback' ) == 'true'
155167 if fallback :
168+ self ._record_stream_init (True )
156169 yield Update (
157170 state = DataSourceState .OFF ,
158171 revert_to_fdv1 = True
@@ -165,6 +178,8 @@ def sync(self, ss: SelectorStore) -> Generator[Update, None, None]:
165178 try :
166179 update = self ._process_message (action , change_set_builder )
167180 if update is not None :
181+ self ._record_stream_init (False )
182+ self ._connection_attempt_start_time = None
168183 yield update
169184 except json .decoder .JSONDecodeError as e :
170185 log .info (
@@ -192,10 +207,6 @@ def sync(self, ss: SelectorStore) -> Generator[Update, None, None]:
192207 environment_id = None , # TODO(sdk-1410)
193208 )
194209
195- # TODO(sdk-1408)
196- # if update is not None:
197- # self._record_stream_init(False)
198-
199210 self ._sse .close ()
200211
201212 def stop (self ):
@@ -207,6 +218,12 @@ def stop(self):
207218 if self ._sse :
208219 self ._sse .close ()
209220
221+ def _record_stream_init (self , failed : bool ):
222+ if self ._diagnostic_accumulator and self ._connection_attempt_start_time :
223+ current_time = int (time () * 1000 )
224+ elapsed = current_time - int (self ._connection_attempt_start_time * 1000 )
225+ self ._diagnostic_accumulator .record_stream_init (current_time , elapsed if elapsed >= 0 else 0 , failed )
226+
210227 # pylint: disable=too-many-return-statements
211228 def _process_message (
212229 self , msg : Event , change_set_builder : ChangeSetBuilder
@@ -301,6 +318,9 @@ def _handle_error(self, error: Exception) -> Tuple[Optional[Update], bool]:
301318
302319 if isinstance (error , json .decoder .JSONDecodeError ):
303320 log .error ("Unexpected error on stream connection: %s, will retry" , error )
321+ self ._record_stream_init (True )
322+ self ._connection_attempt_start_time = time () + \
323+ self ._sse .next_retry_delay # type: ignore
304324
305325 update = Update (
306326 state = DataSourceState .INTERRUPTED ,
@@ -313,6 +333,10 @@ def _handle_error(self, error: Exception) -> Tuple[Optional[Update], bool]:
313333 return (update , True )
314334
315335 if isinstance (error , HTTPStatusError ):
336+ self ._record_stream_init (True )
337+ self ._connection_attempt_start_time = time () + \
338+ self ._sse .next_retry_delay # type: ignore
339+
316340 error_info = DataSourceErrorInfo (
317341 DataSourceErrorKind .ERROR_RESPONSE ,
318342 error .status ,
@@ -344,6 +368,7 @@ def _handle_error(self, error: Exception) -> Tuple[Optional[Update], bool]:
344368 )
345369
346370 if not is_recoverable :
371+ self ._connection_attempt_start_time = None
347372 log .error (http_error_message_result )
348373 self .stop ()
349374 return (update , False )
@@ -352,6 +377,8 @@ def _handle_error(self, error: Exception) -> Tuple[Optional[Update], bool]:
352377 return (update , True )
353378
354379 log .warning ("Unexpected error on stream connection: %s, will retry" , error )
380+ self ._record_stream_init (True )
381+ self ._connection_attempt_start_time = time () + self ._sse .next_retry_delay # type: ignore
355382
356383 update = Update (
357384 state = DataSourceState .INTERRUPTED ,
0 commit comments