Skip to content

Commit d098f9a

Browse files
pvcravenPaul V Cravenjaeopt
authored
[FSSDK-11212] Update code to retry web API calls for fetching datafile and pushing events (#445)
* Update code to retry web API calls for fetching datafile and pushing events * Fix linting issues * Remove print statements * Fix up 'retries' member * Stub out requests.Session.get instead of requests.get * Update tests * Fix mypy error and linting error * Update for tests * Update * Update optimizely/event_dispatcher.py Co-authored-by: Jae Kim <[email protected]> * Update event dispatch to try three times to send events * Update changelog and version number * Update version number * Remove changelog and version update --------- Co-authored-by: Paul V Craven <[email protected]> Co-authored-by: Jae Kim <[email protected]>
1 parent 45e73bb commit d098f9a

9 files changed

+73
-36
lines changed

optimizely/config_manager.py

+28-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import threading
2020
from requests import codes as http_status_codes
2121
from requests import exceptions as requests_exceptions
22+
from requests.adapters import HTTPAdapter
23+
from urllib3.util.retry import Retry
2224

2325
from . import exceptions as optimizely_exceptions
2426
from . import logger as optimizely_logger
@@ -200,6 +202,7 @@ def __init__(
200202
error_handler: Optional[BaseErrorHandler] = None,
201203
notification_center: Optional[NotificationCenter] = None,
202204
skip_json_validation: Optional[bool] = False,
205+
retries: Optional[int] = 3,
203206
):
204207
""" Initialize config manager. One of sdk_key or datafile has to be set to be able to use.
205208
@@ -222,6 +225,7 @@ def __init__(
222225
JSON schema validation will be performed.
223226
224227
"""
228+
self.retries = retries
225229
self._config_ready_event = threading.Event()
226230
super().__init__(
227231
datafile=datafile,
@@ -391,9 +395,18 @@ def fetch_datafile(self) -> None:
391395
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
392396

393397
try:
394-
response = requests.get(
395-
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
396-
)
398+
session = requests.Session()
399+
400+
retries = Retry(total=self.retries,
401+
backoff_factor=0.1,
402+
status_forcelist=[500, 502, 503, 504])
403+
adapter = HTTPAdapter(max_retries=retries)
404+
405+
session.mount('http://', adapter)
406+
session.mount("https://", adapter)
407+
response = session.get(self.datafile_url,
408+
headers=request_headers,
409+
timeout=enums.ConfigManager.REQUEST_TIMEOUT)
397410
except requests_exceptions.RequestException as err:
398411
self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}')
399412
return
@@ -475,9 +488,18 @@ def fetch_datafile(self) -> None:
475488
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
476489

477490
try:
478-
response = requests.get(
479-
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
480-
)
491+
session = requests.Session()
492+
493+
retries = Retry(total=self.retries,
494+
backoff_factor=0.1,
495+
status_forcelist=[500, 502, 503, 504])
496+
adapter = HTTPAdapter(max_retries=retries)
497+
498+
session.mount('http://', adapter)
499+
session.mount("https://", adapter)
500+
response = session.get(self.datafile_url,
501+
headers=request_headers,
502+
timeout=enums.ConfigManager.REQUEST_TIMEOUT)
481503
except requests_exceptions.RequestException as err:
482504
self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}')
483505
return

optimizely/event_dispatcher.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import requests
1919
from requests import exceptions as request_exception
20+
from requests.adapters import HTTPAdapter
21+
from urllib3.util.retry import Retry
2022

2123
from . import event_builder
2224
from .helpers.enums import HTTPVerbs, EventDispatchConfig
@@ -44,11 +46,21 @@ def dispatch_event(event: event_builder.Event) -> None:
4446
event: Object holding information about the request to be dispatched to the Optimizely backend.
4547
"""
4648
try:
49+
session = requests.Session()
50+
51+
retries = Retry(total=EventDispatchConfig.RETRIES,
52+
backoff_factor=0.1,
53+
status_forcelist=[500, 502, 503, 504])
54+
adapter = HTTPAdapter(max_retries=retries)
55+
56+
session.mount('http://', adapter)
57+
session.mount("https://", adapter)
58+
4759
if event.http_verb == HTTPVerbs.GET:
48-
requests.get(event.url, params=event.params,
49-
timeout=EventDispatchConfig.REQUEST_TIMEOUT).raise_for_status()
60+
session.get(event.url, params=event.params,
61+
timeout=EventDispatchConfig.REQUEST_TIMEOUT).raise_for_status()
5062
elif event.http_verb == HTTPVerbs.POST:
51-
requests.post(
63+
session.post(
5264
event.url, data=json.dumps(event.params), headers=event.headers,
5365
timeout=EventDispatchConfig.REQUEST_TIMEOUT,
5466
).raise_for_status()

optimizely/helpers/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ class VersionType:
198198
class EventDispatchConfig:
199199
"""Event dispatching configs."""
200200
REQUEST_TIMEOUT: Final = 10
201+
RETRIES: Final = 3
201202

202203

203204
class OdpEventApiConfig:

optimizely/helpers/validator.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,9 @@ def is_finite_number(value: Any) -> bool:
276276
if math.isnan(value) or math.isinf(value):
277277
return False
278278

279-
if abs(value) > (2 ** 53):
280-
return False
279+
if isinstance(value, (int, float)):
280+
if abs(value) > (2 ** 53):
281+
return False
281282

282283
return True
283284

tests/test_config_manager.py

+14-13
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def test_get_config_blocks(self):
218218
self.assertEqual(1, round(end_time - start_time))
219219

220220

221-
@mock.patch('requests.get')
221+
@mock.patch('requests.Session.get')
222222
class PollingConfigManagerTest(base.BaseTest):
223223
def test_init__no_sdk_key_no_datafile__fails(self, _):
224224
""" Test that initialization fails if there is no sdk_key or datafile provided. """
@@ -379,7 +379,7 @@ def test_fetch_datafile(self, _):
379379
test_response.status_code = 200
380380
test_response.headers = test_headers
381381
test_response._content = test_datafile
382-
with mock.patch('requests.get', return_value=test_response) as mock_request:
382+
with mock.patch('requests.Session.get', return_value=test_response) as mock_request:
383383
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key)
384384
project_config_manager.stop()
385385

@@ -392,7 +392,7 @@ def test_fetch_datafile(self, _):
392392
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
393393

394394
# Call fetch_datafile again and assert that request to URL is with If-Modified-Since header.
395-
with mock.patch('requests.get', return_value=test_response) as mock_requests:
395+
with mock.patch('requests.Session.get', return_value=test_response) as mock_requests:
396396
project_config_manager._initialize_thread()
397397
project_config_manager.start()
398398
project_config_manager.stop()
@@ -421,7 +421,7 @@ def raise_for_status(self):
421421
test_response.headers = test_headers
422422
test_response._content = test_datafile
423423

424-
with mock.patch('requests.get', return_value=test_response) as mock_request:
424+
with mock.patch('requests.Session.get', return_value=test_response) as mock_request:
425425
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger)
426426
project_config_manager.stop()
427427

@@ -434,7 +434,7 @@ def raise_for_status(self):
434434
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
435435

436436
# Call fetch_datafile again, but raise exception this time
437-
with mock.patch('requests.get', return_value=MockExceptionResponse()) as mock_requests:
437+
with mock.patch('requests.Session.get', return_value=MockExceptionResponse()) as mock_requests:
438438
project_config_manager._initialize_thread()
439439
project_config_manager.start()
440440
project_config_manager.stop()
@@ -462,7 +462,7 @@ def test_fetch_datafile__request_exception_raised(self, _):
462462
test_response.status_code = 200
463463
test_response.headers = test_headers
464464
test_response._content = test_datafile
465-
with mock.patch('requests.get', return_value=test_response) as mock_request:
465+
with mock.patch('requests.Session.get', return_value=test_response) as mock_request:
466466
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger)
467467
project_config_manager.stop()
468468

@@ -476,7 +476,7 @@ def test_fetch_datafile__request_exception_raised(self, _):
476476

477477
# Call fetch_datafile again, but raise exception this time
478478
with mock.patch(
479-
'requests.get',
479+
'requests.Session.get',
480480
side_effect=requests.exceptions.RequestException('Error Error !!'),
481481
) as mock_requests:
482482
project_config_manager._initialize_thread()
@@ -506,7 +506,7 @@ def test_fetch_datafile__exception_polling_thread_failed(self, _):
506506
test_response.headers = test_headers
507507
test_response._content = test_datafile
508508

509-
with mock.patch('requests.get', return_value=test_response):
509+
with mock.patch('requests.Session.get', return_value=test_response):
510510
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key,
511511
logger=mock_logger,
512512
update_interval=12345678912345)
@@ -516,8 +516,9 @@ def test_fetch_datafile__exception_polling_thread_failed(self, _):
516516
# verify the error log message
517517
log_messages = [args[0] for args, _ in mock_logger.error.call_args_list]
518518
for message in log_messages:
519+
print(message)
519520
if "Thread for background datafile polling failed. " \
520-
"Error: timestamp too large to convert to C _PyTime_t" not in message:
521+
"Error: timestamp too large to convert to C PyTime_t" not in message:
521522
assert False
522523

523524
def test_is_running(self, _):
@@ -529,7 +530,7 @@ def test_is_running(self, _):
529530
project_config_manager.stop()
530531

531532

532-
@mock.patch('requests.get')
533+
@mock.patch('requests.Session.get')
533534
class AuthDatafilePollingConfigManagerTest(base.BaseTest):
534535
def test_init__datafile_access_token_none__fails(self, _):
535536
""" Test that initialization fails if datafile_access_token is None. """
@@ -569,7 +570,7 @@ def test_fetch_datafile(self, _):
569570
test_response._content = test_datafile
570571

571572
# Call fetch_datafile and assert that request was sent with correct authorization header
572-
with mock.patch('requests.get',
573+
with mock.patch('requests.Session.get',
573574
return_value=test_response) as mock_request:
574575
project_config_manager.fetch_datafile()
575576

@@ -596,7 +597,7 @@ def test_fetch_datafile__request_exception_raised(self, _):
596597
test_response._content = test_datafile
597598

598599
# Call fetch_datafile and assert that request was sent with correct authorization header
599-
with mock.patch('requests.get', return_value=test_response) as mock_request:
600+
with mock.patch('requests.Session.get', return_value=test_response) as mock_request:
600601
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
601602
datafile_access_token=datafile_access_token,
602603
sdk_key=sdk_key,
@@ -614,7 +615,7 @@ def test_fetch_datafile__request_exception_raised(self, _):
614615

615616
# Call fetch_datafile again, but raise exception this time
616617
with mock.patch(
617-
'requests.get',
618+
'requests.Session.get',
618619
side_effect=requests.exceptions.RequestException('Error Error !!'),
619620
) as mock_requests:
620621
project_config_manager._initialize_thread()

tests/test_event_dispatcher.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_dispatch_event__get_request(self):
2929
params = {'a': '111001', 'n': 'test_event', 'g': '111028', 'u': 'oeutest_user'}
3030
event = event_builder.Event(url, params)
3131

32-
with mock.patch('requests.get') as mock_request_get:
32+
with mock.patch('requests.Session.get') as mock_request_get:
3333
event_dispatcher.EventDispatcher.dispatch_event(event)
3434

3535
mock_request_get.assert_called_once_with(url, params=params, timeout=EventDispatchConfig.REQUEST_TIMEOUT)
@@ -46,7 +46,7 @@ def test_dispatch_event__post_request(self):
4646
}
4747
event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'})
4848

49-
with mock.patch('requests.post') as mock_request_post:
49+
with mock.patch('requests.Session.post') as mock_request_post:
5050
event_dispatcher.EventDispatcher.dispatch_event(event)
5151

5252
mock_request_post.assert_called_once_with(
@@ -69,7 +69,7 @@ def test_dispatch_event__handle_request_exception(self):
6969
event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'})
7070

7171
with mock.patch(
72-
'requests.post', side_effect=request_exception.RequestException('Failed Request'),
72+
'requests.Session.post', side_effect=request_exception.RequestException('Failed Request'),
7373
) as mock_request_post, mock.patch('logging.error') as mock_log_error:
7474
event_dispatcher.EventDispatcher.dispatch_event(event)
7575

tests/test_notification_center_registry.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_remove_notification_center(self):
6060
test_response = self.fake_server_response(status_code=200, content=test_datafile)
6161
notification_center = _NotificationCenterRegistry.get_notification_center(sdk_key, logger)
6262

63-
with mock.patch('requests.get', return_value=test_response), \
63+
with mock.patch('requests.Session.get', return_value=test_response), \
6464
mock.patch.object(notification_center, 'send_notifications') as mock_send:
6565

6666
client = Optimizely(sdk_key=sdk_key, logger=logger)

tests/test_optimizely.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -4696,7 +4696,7 @@ def delay(*args, **kwargs):
46964696
time.sleep(.5)
46974697
return mock.DEFAULT
46984698

4699-
with mock.patch('requests.get', return_value=test_response, side_effect=delay):
4699+
with mock.patch('requests.Session.get', return_value=test_response, side_effect=delay):
47004700
# initialize config_manager with delay, so it will receive the datafile after client initialization
47014701
custom_config_manager = config_manager.PollingConfigManager(sdk_key='segments-test', logger=logger)
47024702
client = optimizely.Optimizely(config_manager=custom_config_manager)
@@ -5428,7 +5428,7 @@ def test_send_odp_event__send_event_with_static_config_manager(self):
54285428
def test_send_odp_event__send_event_with_polling_config_manager(self):
54295429
mock_logger = mock.Mock()
54305430
with mock.patch(
5431-
'requests.get',
5431+
'requests.Session.get',
54325432
return_value=self.fake_server_response(
54335433
status_code=200,
54345434
content=json.dumps(self.config_dict_with_audience_segments)
@@ -5467,7 +5467,7 @@ def test_send_odp_event__log_debug_if_datafile_not_ready(self):
54675467
def test_send_odp_event__log_error_if_odp_not_enabled_with_polling_config_manager(self):
54685468
mock_logger = mock.Mock()
54695469
with mock.patch(
5470-
'requests.get',
5470+
'requests.Session.get',
54715471
return_value=self.fake_server_response(
54725472
status_code=200,
54735473
content=json.dumps(self.config_dict_with_audience_segments)

tests/test_optimizely_factory.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from . import base
2727

2828

29-
@mock.patch('requests.get')
29+
@mock.patch('requests.Session.get')
3030
class OptimizelyFactoryTest(base.BaseTest):
3131
def delay(*args, **kwargs):
3232
time.sleep(.5)
@@ -171,7 +171,7 @@ def test_set_batch_size_and_set_flush_interval___should_set_values_valid_or_inva
171171
self.assertEqual(optimizely_instance.event_processor.batch_size, 10)
172172

173173
def test_update_odp_config_correctly(self, _):
174-
with mock.patch('requests.get') as mock_request_post:
174+
with mock.patch('requests.Session.get') as mock_request_post:
175175
mock_request_post.return_value = self.fake_server_response(
176176
status_code=200,
177177
content=json.dumps(self.config_dict_with_audience_segments)
@@ -194,7 +194,7 @@ def test_update_odp_config_correctly_with_custom_config_manager_and_delay(self,
194194
test_datafile = json.dumps(self.config_dict_with_audience_segments)
195195
test_response = self.fake_server_response(status_code=200, content=test_datafile)
196196

197-
with mock.patch('requests.get', return_value=test_response, side_effect=self.delay):
197+
with mock.patch('requests.Session.get', return_value=test_response, side_effect=self.delay):
198198
# initialize config_manager with delay, so it will receive the datafile after client initialization
199199
config_manager = PollingConfigManager(sdk_key='test', logger=logger)
200200
client = OptimizelyFactory.default_instance_with_config_manager(config_manager=config_manager)
@@ -221,7 +221,7 @@ def test_update_odp_config_correctly_with_delay(self, _):
221221
test_datafile = json.dumps(self.config_dict_with_audience_segments)
222222
test_response = self.fake_server_response(status_code=200, content=test_datafile)
223223

224-
with mock.patch('requests.get', return_value=test_response, side_effect=self.delay):
224+
with mock.patch('requests.Session.get', return_value=test_response, side_effect=self.delay):
225225
# initialize config_manager with delay, so it will receive the datafile after client initialization
226226
client = OptimizelyFactory.default_instance(sdk_key='test')
227227
odp_manager = client.odp_manager
@@ -247,7 +247,7 @@ def test_odp_updated_with_custom_instance(self, _):
247247
test_datafile = json.dumps(self.config_dict_with_audience_segments)
248248
test_response = self.fake_server_response(status_code=200, content=test_datafile)
249249

250-
with mock.patch('requests.get', return_value=test_response, side_effect=self.delay):
250+
with mock.patch('requests.Session.get', return_value=test_response, side_effect=self.delay):
251251
# initialize config_manager with delay, so it will receive the datafile after client initialization
252252
client = OptimizelyFactory.custom_instance(sdk_key='test')
253253
odp_manager = client.odp_manager

0 commit comments

Comments
 (0)