Skip to content

Instantly share code, notes, and snippets.

@lucindo
Created September 11, 2016 00:20
Show Gist options
  • Save lucindo/58d891dd4d8eecb07f3f25be06131ac7 to your computer and use it in GitHub Desktop.
Save lucindo/58d891dd4d8eecb07f3f25be06131ac7 to your computer and use it in GitHub Desktop.
Backoff for requests.Session
## 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