Skip to content

Commit 22437a7

Browse files
rashidspaliabbasrizvi
authored andcommitted
feat: Add blocking timeout in polling manager (#211)
1 parent 3731be4 commit 22437a7

File tree

3 files changed

+80
-2
lines changed

3 files changed

+80
-2
lines changed

optimizely/config_manager.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# limitations under the License.
1313

1414
import abc
15+
import numbers
1516
import requests
1617
import threading
1718
import time
@@ -95,6 +96,7 @@ def __init__(self,
9596
notification_center=notification_center)
9697
self._config = None
9798
self.validate_schema = not skip_json_validation
99+
self._config_ready_event = threading.Event()
98100
self._set_config(datafile)
99101

100102
def _set_config(self, datafile):
@@ -133,6 +135,7 @@ def _set_config(self, datafile):
133135
return
134136

135137
self._config = config
138+
self._config_ready_event.set()
136139
self.notification_center.send_notifications(enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE)
137140
self.logger.debug(
138141
'Received new datafile and updated config. '
@@ -145,6 +148,7 @@ def get_config(self):
145148
Returns:
146149
ProjectConfig. None if not set.
147150
"""
151+
148152
return self._config
149153

150154

@@ -155,6 +159,7 @@ def __init__(self,
155159
sdk_key=None,
156160
datafile=None,
157161
update_interval=None,
162+
blocking_timeout=None,
158163
url=None,
159164
url_template=None,
160165
logger=None,
@@ -168,6 +173,8 @@ def __init__(self,
168173
datafile: Optional JSON string representing the project.
169174
update_interval: Optional floating point number representing time interval in seconds
170175
at which to request datafile and set ProjectConfig.
176+
blocking_timeout: Optional Time in seconds to block the get_config call until config object
177+
has been initialized.
171178
url: Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
172179
url_template: Optional string template which in conjunction with sdk_key
173180
determines URL from where to fetch the datafile.
@@ -187,6 +194,7 @@ def __init__(self,
187194
self.datafile_url = self.get_datafile_url(sdk_key, url,
188195
url_template or enums.ConfigManager.DATAFILE_URL_TEMPLATE)
189196
self.set_update_interval(update_interval)
197+
self.set_blocking_timeout(blocking_timeout)
190198
self.last_modified = None
191199
self._polling_thread = threading.Thread(target=self._run)
192200
self._polling_thread.setDaemon(True)
@@ -224,15 +232,26 @@ def get_datafile_url(sdk_key, url, url_template):
224232

225233
return url
226234

235+
def get_config(self):
236+
""" Returns instance of ProjectConfig. Returns immediately if project config is ready otherwise
237+
blocks maximum for value of blocking_timeout in seconds.
238+
239+
Returns:
240+
ProjectConfig. None if not set.
241+
"""
242+
243+
self._config_ready_event.wait(self.blocking_timeout)
244+
return self._config
245+
227246
def set_update_interval(self, update_interval):
228247
""" Helper method to set frequency at which datafile has to be polled and ProjectConfig updated.
229248
230249
Args:
231250
update_interval: Time in seconds after which to update datafile.
232251
"""
233-
if not update_interval:
252+
if update_interval is None:
234253
update_interval = enums.ConfigManager.DEFAULT_UPDATE_INTERVAL
235-
self.logger.debug('Set config update interval to default value {}.'.format(update_interval))
254+
self.logger.debug('Setting config update interval to default value {}.'.format(update_interval))
236255

237256
if not isinstance(update_interval, (int, float)):
238257
raise optimizely_exceptions.InvalidInputException(
@@ -249,6 +268,31 @@ def set_update_interval(self, update_interval):
249268

250269
self.update_interval = update_interval
251270

271+
def set_blocking_timeout(self, blocking_timeout):
272+
""" Helper method to set time in seconds to block the config call until config has been initialized.
273+
274+
Args:
275+
blocking_timeout: Time in seconds to block the config call.
276+
"""
277+
if blocking_timeout is None:
278+
blocking_timeout = enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT
279+
self.logger.debug('Setting config blocking timeout to default value {}.'.format(blocking_timeout))
280+
281+
if not isinstance(blocking_timeout, (numbers.Integral, float)):
282+
raise optimizely_exceptions.InvalidInputException(
283+
'Invalid blocking timeout "{}" provided.'.format(blocking_timeout)
284+
)
285+
286+
# If blocking timeout is less than 0 then set it to default blocking timeout.
287+
if blocking_timeout < 0:
288+
self.logger.debug('blocking timeout value {} too small. Defaulting to {}'.format(
289+
blocking_timeout,
290+
enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT)
291+
)
292+
blocking_timeout = enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT
293+
294+
self.blocking_timeout = blocking_timeout
295+
252296
def set_last_modified(self, response_headers):
253297
""" Looks up and sets last modified time based on Last-Modified header in the response.
254298

optimizely/helpers/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class AudienceEvaluationLogs(object):
3838

3939
class ConfigManager(object):
4040
DATAFILE_URL_TEMPLATE = 'https://cdn.optimizely.com/datafiles/{sdk_key}.json'
41+
# Default time in seconds to block the 'get_config' method call until 'config' instance has been initialized.
42+
DEFAULT_BLOCKING_TIMEOUT = 10
4143
# Default config update interval of 5 minutes
4244
DEFAULT_UPDATE_INTERVAL = 5 * 60
4345
# Time in seconds before which request for datafile times out

tests/test_config_manager.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import json
1515
import mock
1616
import requests
17+
import time
1718

1819
from optimizely import config_manager
1920
from optimizely import exceptions as optimizely_exceptions
@@ -235,6 +236,37 @@ def test_set_update_interval(self, _):
235236
project_config_manager.set_update_interval(42)
236237
self.assertEqual(42, project_config_manager.update_interval)
237238

239+
def test_set_blocking_timeout(self, _):
240+
""" Test set_blocking_timeout with different inputs. """
241+
project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key')
242+
243+
# Assert that if invalid blocking_timeout is set, then exception is raised.
244+
with self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException,
245+
'Invalid blocking timeout "invalid timeout" provided.'):
246+
project_config_manager.set_blocking_timeout('invalid timeout')
247+
248+
# Assert that blocking_timeout cannot be set to less than allowed minimum and instead is set to default value.
249+
project_config_manager.set_blocking_timeout(-4)
250+
self.assertEqual(enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT, project_config_manager.blocking_timeout)
251+
252+
# Assert that blocking_timeout can be set to 0.
253+
project_config_manager.set_blocking_timeout(0)
254+
self.assertIs(0, project_config_manager.blocking_timeout)
255+
256+
# Assert that if no blocking_timeout is provided, it is set to default value.
257+
project_config_manager.set_blocking_timeout(None)
258+
self.assertEqual(enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT, project_config_manager.blocking_timeout)
259+
260+
# Assert that if valid blocking_timeout is provided, it is set to that value.
261+
project_config_manager.set_blocking_timeout(5)
262+
self.assertEqual(5, project_config_manager.blocking_timeout)
263+
264+
# Assert get_config should block until blocking timeout.
265+
start_time = time.time()
266+
project_config_manager.get_config()
267+
end_time = time.time()
268+
self.assertEqual(5, round(end_time - start_time))
269+
238270
def test_set_last_modified(self, _):
239271
""" Test that set_last_modified sets last_modified field based on header. """
240272
project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key')

0 commit comments

Comments
 (0)