Skip to content

Commit aacae06

Browse files
Merge pull request #79 from adorton-adobe/connection_error
Treat connection error like a timeout
2 parents e83d22c + 1af5eb5 commit aacae06

File tree

2 files changed

+84
-28
lines changed

2 files changed

+84
-28
lines changed

tests/test_connections.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ def test_remote_status_timeout():
5959
assert remote_status["status"].startswith("Unreachable")
6060

6161

62+
def test_remote_status_error():
63+
with mock.patch("umapi_client.connection.requests.Session.get") as mock_get:
64+
mock_get.side_effect = requests.ConnectionError
65+
conn = Connection(**mock_connection_params)
66+
_, remote_status = conn.status(remote=True)
67+
assert remote_status["status"].startswith("Unreachable")
68+
69+
70+
# log_stream fixture defined in conftest.py
71+
def test_remote_status_error_logging(log_stream):
72+
with mock.patch("umapi_client.connection.requests.Session.get") as mock_get:
73+
mock_get.side_effect = requests.ConnectionError
74+
stream, logger = log_stream
75+
params = dict(mock_connection_params)
76+
params["logger"] = logger
77+
conn = Connection(**params)
78+
pytest.raises(UnavailableError, conn.make_call, "")
79+
stream.flush()
80+
log = stream.getvalue() # save as a local so can do pytest -l to see exact log
81+
assert "code Error on try 1" in log
82+
assert "code Error on try 2" in log
83+
assert "code Error on try 3" in log
84+
assert "code Error on try 4" not in log
85+
86+
6287
def test_ua_string():
6388
conn = Connection(**mock_connection_params)
6489
req = conn.session.prepare_request(requests.Request('GET', "http://test.com/"))

umapi_client/connection.py

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,44 @@
3434
from .version import __version__ as umapi_version
3535

3636

37+
class APIResult:
38+
success_codes = [200, 201, 204]
39+
timeout_codes = [429, 502, 503, 504]
40+
client_error = lambda self, x: 201 <= x < 200
41+
request_error = lambda self, x: 400 <= x < 500
42+
43+
def __init__(self, result=None, success=False, timeout=None):
44+
self.result = result
45+
self.success = success
46+
self.timeout = timeout
47+
self.status_code = result.status_code if hasattr(result, 'status_code') else 'Error'
48+
49+
def check_result(self):
50+
if self.result.status_code in self.success_codes:
51+
self.success = True
52+
return self
53+
if self.result.status_code in self.timeout_codes:
54+
self.success = False
55+
self.timeout = self.get_timeout()
56+
return self
57+
if self.client_error(self.result.status_code):
58+
raise ClientError("Unexpected HTTP Status {:d}: {}".format(self.result.status_code, self.result.text), self.result)
59+
if self.request_error(self.result.status_code):
60+
raise RequestError(self.result)
61+
raise ServerError(self.result)
62+
63+
def get_timeout(self):
64+
if "Retry-After" in self.result.headers:
65+
advice = self.result.headers["Retry-After"]
66+
advised_time = parsedate_tz(advice)
67+
if advised_time is not None:
68+
# header contains date
69+
return int(mktime_tz(advised_time) - time())
70+
else:
71+
# header contains delta seconds
72+
return int(advice)
73+
return 0
74+
3775
class Connection:
3876
"""
3977
An org-specific, authorized connection to the UMAPI service. Each method
@@ -446,38 +484,31 @@ def call():
446484
start_time = time()
447485
result = None
448486
for num_attempts in range(1, self.retry_max_attempts + 1):
487+
checked_result = None
449488
try:
450489
result = call()
451-
if result.status_code in [200,201,204]:
452-
return result
453-
elif result.status_code in [429, 502, 503, 504]:
454-
if self.logger: self.logger.warning("UMAPI timeout...service unavailable (code %d on try %d)",
455-
result.status_code, num_attempts)
456-
retry_wait = 0
457-
if "Retry-After" in result.headers:
458-
advice = result.headers["Retry-After"]
459-
advised_time = parsedate_tz(advice)
460-
if advised_time is not None:
461-
# header contains date
462-
retry_wait = int(mktime_tz(advised_time) - time())
463-
else:
464-
# header contains delta seconds
465-
retry_wait = int(advice)
466-
if retry_wait <= 0:
467-
# use exponential back-off with random delay
468-
delay = randint(0, self.retry_random_delay)
469-
retry_wait = (int(pow(2, num_attempts - 1)) * self.retry_first_delay) + delay
470-
elif 201 <= result.status_code < 400:
471-
raise ClientError("Unexpected HTTP Status {:d}: {}".format(result.status_code, result.text), result)
472-
elif 400 <= result.status_code < 500:
473-
raise RequestError(result)
474-
else:
475-
raise ServerError(result)
490+
checked_result = APIResult(result).check_result()
476491
except requests.Timeout:
477492
if self.logger: self.logger.warning("UMAPI connection timeout...(%d seconds on try %d)",
478493
self.timeout, num_attempts)
479-
retry_wait = 0
480-
result = None
494+
checked_result = APIResult(success=False, timeout=0)
495+
except requests.ConnectionError:
496+
if self.logger: self.logger.warning("UMAPI connection error...(%d seconds on try %d)",
497+
self.timeout, num_attempts)
498+
checked_result = APIResult(success=False, timeout=0)
499+
500+
if checked_result.success:
501+
return result
502+
503+
if self.logger: self.logger.warning("UMAPI timeout...service unavailable (code %s on try %d)",
504+
checked_result.status_code, num_attempts)
505+
506+
retry_wait = checked_result.timeout
507+
if retry_wait <= 0:
508+
# use exponential back-off with random delay
509+
delay = randint(0, self.retry_random_delay)
510+
retry_wait = (int(pow(2, num_attempts - 1)) * self.retry_first_delay) + delay
511+
481512
if num_attempts < self.retry_max_attempts:
482513
if retry_wait > 0:
483514
if self.logger: self.logger.warning("Next retry in %d seconds...", retry_wait)
@@ -487,4 +518,4 @@ def call():
487518
total_time = int(time() - start_time)
488519
if self.logger: self.logger.error("UMAPI timeout...giving up after %d attempts (%d seconds).",
489520
self.retry_max_attempts, total_time)
490-
raise UnavailableError(self.retry_max_attempts, total_time, result)
521+
raise UnavailableError(self.retry_max_attempts, total_time, checked_result.result)

0 commit comments

Comments
 (0)