Created
September 11, 2016 00:20
-
-
Save lucindo/58d891dd4d8eecb07f3f25be06131ac7 to your computer and use it in GitHub Desktop.
Backoff for requests.Session
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## Backoff for resquests.Session | |
# | |
# A common pattern for me when using the requests library is | |
# to make several HTTP requests to an endpoint using a Session | |
# in order to mantain an open connection to the server. | |
# | |
# Many times I had to set requests.adapters.DEFAULT_RETRIES to | |
# some value in order to avoid transient errors that a simple | |
# retry would take care of. | |
# | |
# Normally my code would look like this: | |
# | |
# requests.adapters.DEFAULT_RETRIES = 10 | |
# session = requests.Session | |
# def function_that_call_endpoint_many_times(): | |
# global session | |
# try: | |
# ... | |
# response = session.get(...) # or post, etc | |
# ... | |
# | |
# That's fine and works great in most cases. But not all errors | |
# are born the same, specially on the microservices era :( | |
# | |
# Today I'm dealing with unstable endpoints that throw 5XX errors | |
# for some seconds and get back to reply ok for the same request after | |
# that. Mostly because of a microservice dependency hell. | |
# | |
# This inspired me to write a propor Backoff implementation for | |
# requests.Session, with configurable retry methods and the option to | |
# deal with internal server errors as transient/connection errors. | |
# | |
# The change on my code was minimal: | |
# | |
# session = BackoffSession(backoff_medthod=ExponentialBackoffMethod(max_value=20.0), max_tries=500, check_server_error=True) | |
# def function_that_call_endpoint_many_times(): | |
# global session | |
# try: | |
# ... | |
# response = session.get(...) # or post, etc | |
# ... | |
# | |
import requests | |
import time | |
class UniformBackoffMethod(object): | |
def __init__(self, interval = 0.5): | |
self._interval = interval | |
def reset(self): | |
pass | |
def next_interval(self): | |
return self._interval | |
class IncrementalBackoffMethod(object): | |
def __init__(self, incremet = 1.0, max_value = None): | |
self._increment = incremet | |
self._max_value = max_value | |
self.reset() | |
def reset(self): | |
self._interval = 0.0 | |
def next_interval(self): | |
self._interval += self._increment | |
if self._max_value is not None and self._interval > self._max_value: | |
self._increment = self._max_value | |
return self._interval | |
class ExponentialBackoffMethod(object): | |
# TODO: read this :P https://en.wikipedia.org/wiki/Exponential_backoff | |
def __init__(self, start = 0.5, multiplier = 1.5, max_value = 300.0): | |
self._start = start | |
self._multiplier = multiplier | |
self._max_value = max_value | |
self.reset() | |
def reset(self): | |
self._interval = self._start | |
def next_interval(self): | |
self._interval *= self._multiplier | |
if self._max_value is not None and self._interval > self._max_value: | |
self._increment = self._max_value | |
return self._interval | |
class BackoffSession(requests.Session): | |
""" This class adds Backoff capabilities to requests.Session | |
Parameters: | |
- backoff_medthod: instance of a BackoffMethod class. Any class with | |
next_interval() and reset() methods will work. | |
See: UniformBackoffMethod, ExponentialBackoffMethod, etc | |
Default: UniformBackoffMethod with zero interval | |
(this has the same effect of setting requests.adapters.DEFAULT_RETRIES) | |
- max_tries: max attempts on a single requests | |
Default: 10 | |
- check_server_error: check if response code is 5XX and assumes it's a transient | |
error that should be retried. | |
Default: True | |
""" | |
def __init__(self, backoff_medthod = UniformBackoffMethod(interval=0.0), max_tries = 10, check_server_error = True): | |
self._max_tries = max_tries | |
self._backoff_medthod = backoff_medthod | |
self._check_server_error = check_server_error | |
super(BackoffSession, self).__init__() # don't know if this is needed | |
self.reset() | |
def reset(self): | |
self._attempts = 0 | |
self._backoff_medthod.reset() | |
def check_for_error(self, response): | |
# when check_server_error is on we assume Internal Server Error is transient | |
# and the request should be retried | |
if self._check_server_error and response.status_code >= 500: | |
return True | |
return False | |
def request(self, method, url, **kwargs): | |
while True: | |
response = None | |
got_error = False | |
try: | |
self._attempts += 1 | |
response = super(BackoffSession, self).request(method, url, **kwargs) | |
except Exception, ex: | |
got_error = True | |
if not got_error: | |
got_error = self.check_for_error(response) | |
if not got_error: | |
self.reset() | |
return response | |
elif self._attempts == self._max_tries: | |
raise Exception("BackoffSession error: max tries exceed (%d attempts)" % self._attempts) | |
else: | |
time.sleep(self._backoff_medthod.next_interval()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment