Skip to content

Commit 9918eba

Browse files
committed
fix(exception-handling): Fix handling of network and other non-status-code errors when polling for datafile
1 parent adb83b2 commit 9918eba

File tree

3 files changed

+109
-9
lines changed

3 files changed

+109
-9
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Build and install the SDK with pip, using the following command:
176176
To get test dependencies installed, use a modified version of the
177177
install command:
178178

179-
pip install -e .[test]
179+
pip install -e '.[test]'
180180

181181
You can run all unit tests with:
182182

optimizely/config_manager.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import requests
1717
import threading
1818
import time
19+
1920
from requests import codes as http_status_codes
2021
from requests import exceptions as requests_exceptions
2122

@@ -344,9 +345,14 @@ def fetch_datafile(self):
344345
if self.last_modified:
345346
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
346347

347-
response = requests.get(
348-
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
349-
)
348+
try:
349+
response = requests.get(
350+
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
351+
)
352+
except requests_exceptions.RequestException as err:
353+
self.logger.error('Fetching datafile from {} failed. Error: {}'.format(self.datafile_url, str(err)))
354+
return
355+
350356
self._handle_response(response)
351357

352358
@property
@@ -411,7 +417,12 @@ def fetch_datafile(self):
411417
if self.last_modified:
412418
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
413419

414-
response = requests.get(
415-
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
416-
)
420+
try:
421+
response = requests.get(
422+
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
423+
)
424+
except requests_exceptions.RequestException as err:
425+
self.logger.error('Fetching datafile from {} failed. Error: {}'.format(self.datafile_url, str(err)))
426+
return
427+
417428
self._handle_response(response)

tests/test_config_manager.py

+91-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# limitations under the License.
1313

1414
import json
15+
1516
import mock
1617
import requests
1718
import time
@@ -394,8 +395,8 @@ def test_fetch_datafile(self, _):
394395
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
395396
self.assertTrue(project_config_manager.is_running)
396397

397-
def test_fetch_datafile__exception_raised(self, _):
398-
""" Test that config_manager keeps running if exception is raised when fetching datafile. """
398+
def test_fetch_datafile__status_exception_raised(self, _):
399+
""" Test that config_manager keeps running if status code exception is raised when fetching datafile. """
399400
class MockExceptionResponse(object):
400401
def raise_for_status(self):
401402
raise requests.exceptions.RequestException('Error Error !!')
@@ -434,6 +435,42 @@ def raise_for_status(self):
434435
# Confirm that config manager keeps running
435436
self.assertTrue(project_config_manager.is_running)
436437

438+
def test_fetch_datafile__request_exception_raised(self, _):
439+
""" Test that config_manager keeps running if a request exception is raised when fetching datafile. """
440+
sdk_key = 'some_key'
441+
mock_logger = mock.Mock()
442+
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
443+
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger)
444+
expected_datafile_url = enums.ConfigManager.DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
445+
test_headers = {'Last-Modified': 'New Time'}
446+
test_datafile = json.dumps(self.config_dict_with_features)
447+
test_response = requests.Response()
448+
test_response.status_code = 200
449+
test_response.headers = test_headers
450+
test_response._content = test_datafile
451+
with mock.patch('requests.get', return_value=test_response):
452+
project_config_manager.fetch_datafile()
453+
454+
self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified)
455+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
456+
457+
# Call fetch_datafile again, but raise exception this time
458+
with mock.patch('requests.get', side_effect=requests.exceptions.RequestException('Error Error !!')) as mock_requests:
459+
project_config_manager.fetch_datafile()
460+
461+
mock_requests.assert_called_once_with(
462+
expected_datafile_url,
463+
headers={'If-Modified-Since': test_headers['Last-Modified']},
464+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
465+
)
466+
mock_logger.error.assert_called_once_with('Fetching datafile from {} failed. Error: Error Error !!'.format(
467+
expected_datafile_url
468+
))
469+
self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified)
470+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
471+
# Confirm that config manager keeps running
472+
self.assertTrue(project_config_manager.is_running)
473+
437474
def test_is_running(self, _):
438475
""" Test that polling thread is running after instance of PollingConfigManager is created. """
439476
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
@@ -492,3 +529,55 @@ def test_fetch_datafile(self, _):
492529
)
493530

494531
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
532+
533+
def test_fetch_datafile__request_exception_raised(self, _):
534+
""" Test that config_manager keeps running if a request exception is raised when fetching datafile. """
535+
datafile_access_token = 'some_token'
536+
sdk_key = 'some_key'
537+
mock_logger = mock.Mock()
538+
539+
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile'):
540+
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
541+
datafile_access_token=datafile_access_token, sdk_key=sdk_key, logger=mock_logger)
542+
expected_datafile_url = enums.ConfigManager.AUTHENTICATED_DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
543+
test_headers = {'Last-Modified': 'New Time'}
544+
test_datafile = json.dumps(self.config_dict_with_features)
545+
test_response = requests.Response()
546+
test_response.status_code = 200
547+
test_response.headers = test_headers
548+
test_response._content = test_datafile
549+
550+
# Call fetch_datafile and assert that request was sent with correct authorization header
551+
with mock.patch('requests.get',
552+
return_value=test_response) as mock_request:
553+
project_config_manager.fetch_datafile()
554+
555+
mock_request.assert_called_once_with(
556+
expected_datafile_url,
557+
headers={'Authorization': 'Bearer {datafile_access_token}'.format(
558+
datafile_access_token=datafile_access_token)},
559+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
560+
)
561+
562+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
563+
564+
# Call fetch_datafile again, but raise exception this time
565+
with mock.patch('requests.get', side_effect=requests.exceptions.RequestException('Error Error !!')) as mock_requests:
566+
project_config_manager.fetch_datafile()
567+
568+
mock_requests.assert_called_once_with(
569+
expected_datafile_url,
570+
headers={
571+
'If-Modified-Since': test_headers['Last-Modified'],
572+
'Authorization': 'Bearer {datafile_access_token}'.format(
573+
datafile_access_token=datafile_access_token),
574+
},
575+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
576+
)
577+
mock_logger.error.assert_called_once_with('Fetching datafile from {} failed. Error: Error Error !!'.format(
578+
expected_datafile_url
579+
))
580+
self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified)
581+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
582+
# Confirm that config manager keeps running
583+
self.assertTrue(project_config_manager.is_running)

0 commit comments

Comments
 (0)