Skip to content

Commit 4461bd6

Browse files
authored
DAS-2427: Adds retry logic to harmony requests Session (#119)
1 parent 2c930dc commit 4461bd6

File tree

2 files changed

+106
-25
lines changed

2 files changed

+106
-25
lines changed

harmony/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import platform
2929
from uuid import UUID
3030
from requests import Response
31+
from requests.adapters import HTTPAdapter
32+
from urllib3.util.retry import Retry
3133
from requests.exceptions import JSONDecodeError
3234
import requests.models
3335
from concurrent.futures import Future, ThreadPoolExecutor
@@ -164,6 +166,20 @@ def _session(self):
164166
self.session = create_session(self.config, token=self.token)
165167
else:
166168
self.session = create_session(self.config, auth=self.auth)
169+
# Add retry logic
170+
retry_strategy = Retry(
171+
total=3,
172+
backoff_factor=1, # Wait 1, 2, 4 seconds between retries
173+
backoff_jitter=0.5,
174+
status_forcelist=[429, 500, 502, 503, 504],
175+
allowed_methods=["GET"],
176+
raise_on_status=False,
177+
178+
)
179+
adapter = HTTPAdapter(max_retries=retry_strategy)
180+
self.session.mount("https://", adapter)
181+
self.session.mount("http://", adapter)
182+
167183
return self.session
168184

169185
def _http_method(self, request: BaseRequest) -> str:

tests/test_client.py

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import dateutil.parser
1111
import pytest
1212
import responses
13+
from responses import registries
1314

15+
from urllib3.util.retry import Retry
1416
from harmony.request import BBox, Collection, LinkType, Request, Dimension, CapabilitiesRequest, \
1517
AddLabelsRequest, DeleteLabelsRequest, JobsRequest
1618
from harmony.client import Client, ProcessingFailedException, DEFAULT_JOB_LABEL
@@ -1482,12 +1484,6 @@ def test_handle_error_response_with_description_key():
14821484
status=500,
14831485
json=error
14841486
)
1485-
responses.add(
1486-
responses.GET,
1487-
expected_status_url(job_id),
1488-
status=500,
1489-
json=error
1490-
)
14911487
with pytest.raises(Exception) as e:
14921488
Client(should_validate_auth=False).submit(request)
14931489
assert str(e.value) == f"('Internal Server Error', '{error['description']}')"
@@ -1499,6 +1495,7 @@ def test_handle_error_response_with_description_key():
14991495
with pytest.raises(Exception) as e:
15001496
Client(should_validate_auth=False).progress(job_id)
15011497
assert str(e.value) == f"('Internal Server Error', '{error['description']}')"
1498+
assert len(responses.calls) == 9
15021499

15031500
@responses.activate
15041501
def test_handle_error_response_no_description_key():
@@ -1521,12 +1518,6 @@ def test_handle_error_response_no_description_key():
15211518
status=500,
15221519
json=error
15231520
)
1524-
responses.add(
1525-
responses.GET,
1526-
expected_status_url(job_id),
1527-
status=500,
1528-
json=error
1529-
)
15301521
with pytest.raises(Exception) as e:
15311522
Client(should_validate_auth=False).submit(request)
15321523
assert "500 Server Error: Internal Server Error for url" in str(e.value)
@@ -1538,6 +1529,8 @@ def test_handle_error_response_no_description_key():
15381529
with pytest.raises(Exception) as e:
15391530
Client(should_validate_auth=False).progress(job_id)
15401531
assert "500 Server Error: Internal Server Error for url" in str(e.value)
1532+
# Check retries
1533+
assert len(responses.calls) == 9 # (1 + 4 + 4) # Post isn't retried.
15411534

15421535
@responses.activate
15431536
def test_handle_error_response_no_json():
@@ -1559,12 +1552,6 @@ def test_handle_error_response_no_json():
15591552
status=500,
15601553
body='error'
15611554
)
1562-
responses.add(
1563-
responses.GET,
1564-
expected_status_url(job_id),
1565-
status=500,
1566-
body='error'
1567-
)
15681555
with pytest.raises(Exception) as e:
15691556
Client(should_validate_auth=False).submit(request)
15701557
assert "500 Server Error: Internal Server Error for url" in str(e.value)
@@ -1576,6 +1563,9 @@ def test_handle_error_response_no_json():
15761563
with pytest.raises(Exception) as e:
15771564
Client(should_validate_auth=False).progress(job_id)
15781565
assert "500 Server Error: Internal Server Error for url" in str(e.value)
1566+
# Check retries
1567+
assert len(responses.calls) == 9 # (1 + 4 + 4)
1568+
15791569

15801570
@responses.activate
15811571
def test_handle_error_response_invalid_json():
@@ -1597,12 +1587,6 @@ def test_handle_error_response_invalid_json():
15971587
status=500,
15981588
json='error'
15991589
)
1600-
responses.add(
1601-
responses.GET,
1602-
expected_status_url(job_id),
1603-
status=500,
1604-
json='error'
1605-
)
16061590
with pytest.raises(Exception) as e:
16071591
Client(should_validate_auth=False).submit(request)
16081592
assert "500 Server Error: Internal Server Error for url" in str(e.value)
@@ -1614,6 +1598,87 @@ def test_handle_error_response_invalid_json():
16141598
with pytest.raises(Exception) as e:
16151599
Client(should_validate_auth=False).progress(job_id)
16161600
assert "500 Server Error: Internal Server Error for url" in str(e.value)
1601+
# Check retries
1602+
assert len(responses.calls) == 9 # (1 + 4 + 4)
1603+
1604+
@responses.activate
1605+
def test_handle_non_transient_error_no_retry():
1606+
job_id = '89733-badc-1324'
1607+
collection = Collection(id='F229040468263-STRP')
1608+
request = Request(
1609+
collection=collection,
1610+
spatial=BBox(-107, 40, -105, 42)
1611+
)
1612+
responses.add(
1613+
responses.POST,
1614+
expected_submit_url(collection.id),
1615+
status=200,
1616+
json=expected_job(collection.id, 'abcd-1234')
1617+
)
1618+
responses.add(
1619+
responses.GET,
1620+
expected_status_url(job_id),
1621+
status=431,
1622+
json='no-retry error'
1623+
)
1624+
1625+
Client(should_validate_auth=False).submit(request)
1626+
1627+
with pytest.raises(Exception) as e:
1628+
Client(should_validate_auth=False).status(job_id)
1629+
assert "431 Client Error: Request Header Fields Too Large for url" in str(e.value)
1630+
1631+
# Check no retries
1632+
assert len(responses.calls) == 2
1633+
1634+
@responses.activate(registry=registries.OrderedRegistry)
1635+
def test_handle_transient_error_responses():
1636+
job_id = '3141592653-acbd-1234'
1637+
collection = Collection(id='C1342468263-ANYTHING')
1638+
request = Request(
1639+
collection=collection,
1640+
spatial=BBox(-107, 40, -105, 42)
1641+
)
1642+
responses.add(
1643+
responses.POST,
1644+
expected_submit_url(collection.id),
1645+
status=200,
1646+
json=expected_job(collection.id, 'abcd-1234'),
1647+
)
1648+
first_success = responses.add(
1649+
responses.GET,
1650+
expected_status_url(job_id),
1651+
status=200,
1652+
json=expected_job(collection.id, job_id)
1653+
)
1654+
first_retry = responses.add(
1655+
responses.GET,
1656+
expected_status_url(job_id),
1657+
status=502,
1658+
json='error'
1659+
)
1660+
second_retry = responses.add(
1661+
responses.GET,
1662+
expected_status_url(job_id),
1663+
status=502,
1664+
json='error'
1665+
)
1666+
last_success = responses.add(
1667+
responses.GET,
1668+
expected_status_url(job_id),
1669+
status=200,
1670+
json=expected_job(collection.id, job_id)
1671+
)
1672+
1673+
Client(should_validate_auth=False).submit(request)
1674+
Client(should_validate_auth=False).status(job_id)
1675+
Client(should_validate_auth=False).status(job_id)
1676+
1677+
assert first_success.call_count == 1
1678+
assert first_retry.call_count == 1
1679+
assert second_retry.call_count == 1
1680+
assert last_success.call_count == 1
1681+
16171682

16181683
def test_request_as_curl_get():
16191684
collection = Collection(id='C1940468263-POCLOUD')
@@ -1845,4 +1910,4 @@ def test_client_custom_environment_not_affected_by_env_var():
18451910
assert client.config.environment == Environment.UAT
18461911
assert client.config.harmony_hostname == 'harmony.uat.earthdata.nasa.gov'
18471912

1848-
del os.environ['ENVIRONMENT']
1913+
del os.environ['ENVIRONMENT']

0 commit comments

Comments
 (0)