44"""
55
66import json
7+ from abc import abstractmethod
78from collections import namedtuple
8- from typing import Iterable , Optional
9+ from threading import Event
10+ from time import time
11+ from typing import Generator , Optional , Protocol
912from urllib import parse
1013
1114import urllib3
1215
13- from ldclient .impl .datasourcev2 import PollingRequester , PollingResult , Update
16+ from ldclient .impl .datasourcev2 import BasisResult , PollingResult , Update
1417from ldclient .impl .datasystem .protocolv2 import (
1518 Basis ,
1619 ChangeSet ,
2528from ldclient .impl .http import _http_factory
2629from ldclient .impl .repeating_task import RepeatingTask
2730from ldclient .impl .util import (
28- Result ,
2931 UnsuccessfulResponseException ,
3032 _Fail ,
3133 _headers ,
3537 is_http_error_recoverable ,
3638 log
3739)
40+ from ldclient .interfaces import (
41+ DataSourceErrorInfo ,
42+ DataSourceErrorKind ,
43+ DataSourceState
44+ )
3845
3946POLLING_ENDPOINT = "/sdk/poll"
4047
4148
4249CacheEntry = namedtuple ("CacheEntry" , ["data" , "etag" ])
4350
4451
52+ class Requester (Protocol ): # pylint: disable=too-few-public-methods
53+ """
54+ Requester allows PollingDataSource to delegate fetching data to
55+ another component.
56+
57+ This is useful for testing the PollingDataSource without needing to set up
58+ a test HTTP server.
59+ """
60+
61+ @abstractmethod
62+ def fetch (self , selector : Optional [Selector ]) -> PollingResult :
63+ """
64+ Fetches the data for the given selector.
65+ Returns a Result containing a tuple of ChangeSet and any request headers,
66+ or an error if the data could not be retrieved.
67+ """
68+ raise NotImplementedError
69+
70+
4571class PollingDataSource :
4672 """
4773 PollingDataSource is a data source that can retrieve information from
@@ -51,9 +77,11 @@ class PollingDataSource:
5177 def __init__ (
5278 self ,
5379 poll_interval : float ,
54- requester : PollingRequester ,
80+ requester : Requester ,
5581 ):
5682 self ._requester = requester
83+ self ._poll_interval = poll_interval
84+ self ._event = Event ()
5785 self ._task = RepeatingTask (
5886 "ldclient.datasource.polling" , poll_interval , 0 , self ._poll
5987 )
@@ -62,21 +90,73 @@ def name(self) -> str:
6290 """Returns the name of the initializer."""
6391 return "PollingDataSourceV2"
6492
65- def fetch (self ) -> Result : # Result[Basis] :
93+ def fetch (self ) -> BasisResult :
6694 """
6795 Fetch returns a Basis, or an error if the Basis could not be retrieved.
6896 """
6997 return self ._poll ()
7098
71- # TODO(fdv2): This will need to be converted into a synchronizer at some point.
72- # def start(self):
73- # log.info(
74- # "Starting PollingUpdateProcessor with request interval: "
75- # + str(self._config.poll_interval)
76- # )
77- # self._task.start()
99+ def sync (self ) -> Generator [Update , None , None ]:
100+ """
101+ sync begins the synchronization process for the data source, yielding
102+ Update objects until the connection is closed or an unrecoverable error
103+ occurs.
104+ """
105+ log .info ("Starting PollingDataSourceV2 synchronizer" )
106+ while True :
107+ result = self ._requester .fetch (None )
108+ if isinstance (result , _Fail ):
109+ if isinstance (result .exception , UnsuccessfulResponseException ):
110+ error_info = DataSourceErrorInfo (
111+ kind = DataSourceErrorKind .ERROR_RESPONSE ,
112+ status_code = result .exception .status ,
113+ time = time (),
114+ message = http_error_message (
115+ result .exception .status , "polling request"
116+ ),
117+ )
118+
119+ status_code = result .exception .status
120+ if is_http_error_recoverable (status_code ):
121+ # TODO(fdv2): Add support for environment ID
122+ yield Update (
123+ state = DataSourceState .INTERRUPTED ,
124+ error = error_info ,
125+ )
126+ continue
127+
128+ # TODO(fdv2): Add support for environment ID
129+ yield Update (
130+ state = DataSourceState .OFF ,
131+ error = error_info ,
132+ )
133+ break
78134
79- def _poll (self ) -> Result : # Result[Basis]:
135+ error_info = DataSourceErrorInfo (
136+ kind = DataSourceErrorKind .NETWORK_ERROR ,
137+ time = time (),
138+ status_code = 0 ,
139+ message = result .error ,
140+ )
141+
142+ # TODO(fdv2): Go has a designation here to handle JSON decoding separately.
143+ # TODO(fdv2): Add support for environment ID
144+ yield Update (
145+ state = DataSourceState .INTERRUPTED ,
146+ error = error_info ,
147+ )
148+ else :
149+ (change_set , headers ) = result .value
150+ yield Update (
151+ state = DataSourceState .VALID ,
152+ change_set = change_set ,
153+ environment_id = headers .get ("X-LD-EnvID" ),
154+ )
155+
156+ if self ._event .wait (self ._poll_interval ):
157+ break
158+
159+ def _poll (self ) -> BasisResult :
80160 try :
81161 # TODO(fdv2): Need to pass the selector through
82162 result = self ._requester .fetch (None )
@@ -90,10 +170,13 @@ def _poll(self) -> Result: # Result[Basis]:
90170 if is_http_error_recoverable (status_code ):
91171 log .warning (http_error_message_result )
92172
93- return Result .fail (http_error_message_result , result .exception )
173+ return _Fail (
174+ error = http_error_message_result , exception = result .exception
175+ )
94176
95- return Result .fail (
96- result .error or "Failed to request payload" , result .exception
177+ return _Fail (
178+ error = result .error or "Failed to request payload" ,
179+ exception = result .exception ,
97180 )
98181
99182 (change_set , headers ) = result .value
@@ -108,18 +191,19 @@ def _poll(self) -> Result: # Result[Basis]:
108191 environment_id = env_id ,
109192 )
110193
111- return Result . success ( basis )
112- except Exception as e :
194+ return _Success ( value = basis )
195+ except Exception as e : # pylint: disable=broad-except
113196 msg = f"Error: Exception encountered when updating flags. { e } "
114197 log .exception (msg )
115198
116- return Result . fail ( msg , e )
199+ return _Fail ( error = msg , exception = e )
117200
118201
119202# pylint: disable=too-few-public-methods
120203class Urllib3PollingRequester :
121204 """
122- Urllib3PollingRequester is a PollingRequester that uses urllib3 to make HTTP requests.
205+ Urllib3PollingRequester is a Requester that uses urllib3 to make HTTP
206+ requests.
123207 """
124208
125209 def __init__ (self , config ):
0 commit comments