-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DAS-2034 - Remove custom retry logic from HOSS in favour of harmony-service-lib. #4
Changes from all commits
0b45c1f
828cd16
0a89db0
cbd5153
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
1.0.0 | ||
1.0.1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,10 +15,7 @@ | |
from harmony.exceptions import ForbiddenException, ServerException | ||
from harmony.util import Config, download as util_download | ||
|
||
from hoss.exceptions import UrlAccessFailed, UrlAccessFailedWithRetries | ||
|
||
|
||
HTTP_REQUEST_ATTEMPTS = 3 | ||
from hoss.exceptions import UrlAccessFailed | ||
|
||
|
||
def get_file_mimetype(file_name: str) -> Tuple[Optional[str], Optional[str]]: | ||
|
@@ -90,12 +87,12 @@ def download_url(url: str, destination: str, logger: Logger, | |
access_token: str = None, config: Config = None, | ||
data=None) -> str: | ||
""" Use built-in Harmony functionality to download from a URL. This is | ||
expected to be used for obtaining the granule `.dmr` and the granule | ||
itself (only the required variables). | ||
expected to be used for obtaining the granule `.dmr`, a prefetch of | ||
only dimensions and bounds variables, and the subsetted granule itself. | ||
|
||
OPeNDAP can return intermittent 500 errors. This function will retry | ||
the original request in the event of a 500 error, but not for other | ||
error types. In those instances, the original HTTPError is re-raised. | ||
OPeNDAP can return intermittent 500 errors. Retries will be performed | ||
by inbuilt functionality in the `harmony-service-lib`. The OPeNDAP | ||
errors are captured and re-raised as custom exceptions. | ||
|
||
The return value is the location in the file-store of the downloaded | ||
content from the URL. | ||
|
@@ -106,32 +103,21 @@ def download_url(url: str, destination: str, logger: Logger, | |
if data is not None: | ||
logger.info(f'POST request data: "{format_dictionary_string(data)}"') | ||
|
||
request_completed = False | ||
attempts = 0 | ||
|
||
while not request_completed and attempts < HTTP_REQUEST_ATTEMPTS: | ||
attempts += 1 | ||
|
||
try: | ||
response = util_download( | ||
url, | ||
destination, | ||
logger, | ||
access_token=access_token, | ||
data=data, | ||
cfg=config | ||
) | ||
request_completed = True | ||
except ForbiddenException as harmony_exception: | ||
raise UrlAccessFailed(url, 400) from harmony_exception | ||
except ServerException as harmony_exception: | ||
if attempts < HTTP_REQUEST_ATTEMPTS: | ||
logger.info('500 error returned, retrying request.') | ||
else: | ||
raise UrlAccessFailedWithRetries(url) from harmony_exception | ||
except Exception as harmony_exception: | ||
# Not a 500 error, so raise immediately and exit the loop. | ||
raise UrlAccessFailed(url, 'Unknown') from harmony_exception | ||
try: | ||
response = util_download( | ||
url, | ||
destination, | ||
logger, | ||
access_token=access_token, | ||
data=data, | ||
cfg=config | ||
) | ||
except ForbiddenException as harmony_exception: | ||
raise UrlAccessFailed(url, 400) from harmony_exception | ||
except ServerException as harmony_exception: | ||
raise UrlAccessFailed(url, 500) from harmony_exception | ||
except Exception as harmony_exception: | ||
raise UrlAccessFailed(url, 'Unknown') from harmony_exception | ||
|
||
return response | ||
|
||
|
@@ -159,22 +145,3 @@ def get_value_or_default(value: Optional[float], default: float) -> float: | |
|
||
""" | ||
return value if value is not None else default | ||
|
||
|
||
def rgetattr(input_object, requested_attribute, *args): | ||
""" This is a recursive version of the inbuilt `getattr` method, such that | ||
it can be called to retrieve nested attributes. For example: | ||
the Message.subset.shape within the input Harmony message. | ||
Comment on lines
-162
to
-167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I saw this yesterday browsing the code. |
||
|
||
Note, if a default value is specified, this will be returned if any | ||
attribute in the specified chain is absent from the supplied object. | ||
|
||
""" | ||
if '.' not in requested_attribute: | ||
result = getattr(input_object, requested_attribute, *args) | ||
else: | ||
attribute_pieces = requested_attribute.split('.') | ||
result = rgetattr(getattr(input_object, attribute_pieces[0], *args), | ||
'.'.join(attribute_pieces[1:]), *args) | ||
|
||
return result |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,13 +5,12 @@ | |
from harmony.exceptions import ForbiddenException, ServerException | ||
from harmony.util import config | ||
|
||
from hoss.exceptions import UrlAccessFailed, UrlAccessFailedWithRetries | ||
from hoss.exceptions import UrlAccessFailed | ||
from hoss.utilities import (download_url, format_dictionary_string, | ||
format_variable_set_string, | ||
get_constraint_expression, get_file_mimetype, | ||
get_opendap_nc4, get_value_or_default, | ||
HTTP_REQUEST_ATTEMPTS, move_downloaded_nc4, | ||
rgetattr) | ||
move_downloaded_nc4) | ||
|
||
|
||
class TestUtilities(TestCase): | ||
|
@@ -42,11 +41,9 @@ def test_get_file_mimetype(self): | |
|
||
@patch('hoss.utilities.util_download') | ||
def test_download_url(self, mock_util_download): | ||
""" Ensure that the `harmony.util.download` function is called. Also | ||
ensure that if a 500 error is returned, the request is retried. If | ||
a different HTTPError occurs, the caught HTTPError should be | ||
re-raised. Finally, check the maximum number of request attempts is | ||
not exceeded. | ||
""" Ensure that the `harmony.util.download` function is called. If an | ||
error occurs, the caught exception should be re-raised with a | ||
custom exception with a human-readable error message. | ||
|
||
""" | ||
output_directory = 'output/dir' | ||
|
@@ -88,14 +85,22 @@ def test_download_url(self, mock_util_download): | |
) | ||
mock_util_download.reset_mock() | ||
|
||
with self.subTest('500 error triggers a retry.'): | ||
with self.subTest('500 error is caught and handled.'): | ||
mock_util_download.side_effect = [self.harmony_500_error, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This subtest name is incorrect now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I updated that test name (and also the doc string for the overall test that mentioned retries would be tested) |
||
http_response] | ||
|
||
response = download_url(test_url, output_directory, self.logger) | ||
with self.assertRaises(UrlAccessFailed): | ||
download_url(test_url, output_directory, self.logger, | ||
access_token, self.config) | ||
|
||
self.assertEqual(response, http_response) | ||
self.assertEqual(mock_util_download.call_count, 2) | ||
mock_util_download.assert_called_once_with( | ||
test_url, | ||
output_directory, | ||
self.logger, | ||
access_token=access_token, | ||
data=None, | ||
cfg=self.config | ||
) | ||
mock_util_download.reset_mock() | ||
|
||
with self.subTest('Non-500 error does not retry, and is re-raised.'): | ||
|
@@ -116,17 +121,6 @@ def test_download_url(self, mock_util_download): | |
) | ||
mock_util_download.reset_mock() | ||
|
||
with self.subTest('Maximum number of attempts not exceeded.'): | ||
mock_util_download.side_effect = [ | ||
self.harmony_500_error | ||
] * (HTTP_REQUEST_ATTEMPTS + 1) | ||
with self.assertRaises(UrlAccessFailedWithRetries): | ||
download_url(test_url, output_directory, self.logger) | ||
|
||
self.assertEqual(mock_util_download.call_count, | ||
HTTP_REQUEST_ATTEMPTS) | ||
mock_util_download.reset_mock() | ||
|
||
@patch('hoss.utilities.move_downloaded_nc4') | ||
@patch('hoss.utilities.util_download') | ||
def test_get_opendap_nc4(self, mock_download, mock_move_download): | ||
|
@@ -259,50 +253,3 @@ def test_get_value_or_default(self): | |
|
||
with self.subTest('Value = None returns the supplied default'): | ||
self.assertEqual(get_value_or_default(None, 20), 20) | ||
|
||
def test_rgetattr(self): | ||
""" Ensure that the recursive function can retrieve nested attributes | ||
and uses the default argument when required. | ||
|
||
""" | ||
class Child: | ||
def __init__(self, name): | ||
self.name = name | ||
|
||
class Parent: | ||
def __init__(self, name, child_name): | ||
self.name = name | ||
self.child = Child(child_name) | ||
self.none = None | ||
|
||
test_parent = Parent('parent_name', 'child_name') | ||
|
||
with self.subTest('Parent level attribute'): | ||
self.assertEqual(rgetattr(test_parent, 'name'), 'parent_name') | ||
|
||
with self.subTest('Nested attribute'): | ||
self.assertEqual(rgetattr(test_parent, 'child.name'), 'child_name') | ||
with self.subTest('Missing parent with default'): | ||
self.assertEqual(rgetattr(test_parent, 'absent', 'default'), | ||
'default') | ||
|
||
with self.subTest('Missing child attribute with default'): | ||
self.assertEqual(rgetattr(test_parent, 'child.absent', 'default'), | ||
'default') | ||
with self.subTest('Child requested, parent missing, default'): | ||
self.assertEqual( | ||
rgetattr(test_parent, 'none.something', 'default'), | ||
'default' | ||
) | ||
|
||
with self.subTest('Missing parent, with no default'): | ||
with self.assertRaises(AttributeError): | ||
rgetattr(test_parent, 'absent') | ||
|
||
with self.subTest('Missing child, with no default'): | ||
with self.assertRaises(AttributeError): | ||
rgetattr(test_parent, 'child.absent') | ||
|
||
with self.subTest('Child requested, parent missing, no default'): | ||
with self.assertRaises(AttributeError): | ||
rgetattr(test_parent, 'none.something') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially I included the path to the shape file, but this is somewhat meaningless as Harmony creates a temporary file, which obfuscates any original user naming convention (the file name is essentially a generated ID).