The entire question essentially spins around and calls for a good "Retry/MaxRetry nextwork requests" strategy.
And just for this we have a flexible and reach Retry configuration - urllib3.util.Retry
, which in combination with well-known requests
lib provides granular control over the conditions under which we retry a request.
Among a bunch of options it provides:
class urllib3.util.Retry(total=10, connect=None, read=None,
redirect=None, status=None, method_whitelist=frozenset(['HEAD',
'TRACE', 'GET', 'PUT', 'OPTIONS', 'DELETE']), status_forcelist=None,
backoff_factor=0, raise_on_redirect=True, raise_on_status=True,
history=None, respect_retry_after_header=True,
remove_headers_on_redirect=frozenset(['Authorization']))
I'll mention just some crucial ones:
total
. Total number of retries to allow.
status_forcelist(iterable)
. A set of integer HTTP status codes that we should force a retry on.
In my sample scheme below I used all error HTTP codes to be considered here. But you can restrict that iterable by passing the argument with selected codes like status_forcelist=(500, 502, 503, 504)
.
backoff_factor
. A backoff factor to apply between attempts after the second try (most errors are resolved immediately by a second try without a delay). urllib3
will sleep for:
{backoff factor} * (2 ** ({number of total retries} - 1))
seconds. If the backoff_factor is 0.1
, then sleep()
will sleep for [0.0s, 0.2s, 0.4s, …]
between retries. It will never be longer than Retry.BACKOFF_MAX
.
This would be a more "thought-out" alternative to a constant delay.
To establish the above Retry/MaxRetry strategy we run through the following steps:
- creating instance of
Retry
component with the needed options
- creating
requests.adapters.HTTPAdapter
adapter instance with passing in created Retry
component as its max_retries
parameter: HTTPAdapter(max_retries=self._retry)
- creating requests session with
self.session = requests.Session()
and mounting it to a base url (self.session.mount(self.base_url, self._adapter)
). You can mount session to multiple base urls/prefixes.
Since you posted an instance method def getRequest(self, ...
I assume that that's the context of some API client/wrapper.
Below is a generic sample scheme of using the described strategy. So you can easily adjust/extend your API client class appropriately (and get rid of loops or recursion in "Retry" intention).
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import RetryError
from urllib3.exceptions import MaxRetryError
from urllib3.util.retry import Retry
import datetime
# .... your constants/variables
class MyAPIClient:
ERROR_CODES = tuple(code for code in requests.status_codes._codes if code >= 400)
def __init__(self, base_url, max_retries=5, backoff_factor=0.2):
self.base_url = base_url
self._max_retries = max_retries
self._retry = Retry(total=max_retries,
backoff_factor=backoff_factor,
status_forcelist=MyAPIClient.ERROR_CODES)
self._adapter = HTTPAdapter(max_retries=self._retry)
self.session = requests.Session()
self.session.mount(self.base_url, self._adapter)
def get_request(self, endpoint):
url = self.base_url + endpoint
if self.expirationT <= datetime.datetime.now():
self.token, self.expirationT = self.auth(client_id, client_secret)
try:
response = self.session.get(url, auth=BearerAuth(self.token))
except (MaxRetryError, RetryError) as ex:
# optional actions/logging here. Otherwise - try/except can be just eliminated
raise
return response