Skip to content

Commit

Permalink
Enhancement: py3 only, and RetryHandler class support (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
jheld authored Mar 12, 2020
1 parent f60f88b commit 8b5c187
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 52 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: python
sudo: false
python:
- '2.7'
- '3.5'
- '3.6'
- '3.7'
Expand Down
2 changes: 1 addition & 1 deletion retry_decorator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
from .retry_decorator import *

__title__ = 'retry_decorator'
__version__ = "1.1.1"
__version__ = "2.0.a1"
118 changes: 77 additions & 41 deletions retry_decorator/retry_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,90 @@
# Copyright: Patrick Ng - 2012
#

from __future__ import print_function

import traceback
import logging
import time
import random
import sys


def retry(ExceptionToCheck, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None):
def _deco_retry(f, exc=Exception, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None):
"""
Common function logic for the internal retry flows.
:param f:
:param exc:
:param tries:
:param timeout_secs:
:param logger:
:param callback_by_exception:
:return:
"""
def f_retry(*args, **kwargs):
mtries, mdelay = tries, timeout_secs
run_one_last_time = True
while mtries > 1:
try:
return f(*args, **kwargs)
except exc as e:
# check if this exception is something the caller wants special handling for
callback_errors = callback_by_exception or {}
for error_type in callback_errors:
if isinstance(e, error_type):
callback_logic = callback_by_exception[error_type]
should_break_out = run_one_last_time = False
if isinstance(callback_logic, (list, tuple)):
callback_logic, should_break_out = callback_logic
if isinstance(should_break_out, (list, tuple)):
should_break_out, run_one_last_time = should_break_out
callback_logic()
if should_break_out: # caller requests we stop handling this exception
break
half_interval = mdelay * 0.10 # interval size
actual_delay = random.uniform(mdelay - half_interval, mdelay + half_interval)
msg = "Retrying in %.2f seconds ..." % actual_delay
logging_object = logger or logging
logging_object.exception(msg)
time.sleep(actual_delay)
mtries -= 1
mdelay *= 2
if run_one_last_time: # one exception may be all the caller wanted in certain cases
return f(*args, **kwargs)

return f_retry # true decorator


def retry(exc=Exception, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None):
"""
Retry calling the decorated function using an exponential backoff.
:param exc: catch all exceptions, a specific exception, or an iterable of exceptions
:param tries: how many attempts to retry when catching those exceptions
:param timeout_secs: general delay between retries (we do employ a jitter)
:param logger: an optional logger object
:param callback_by_exception: callback/method invocation on certain exceptions
:type callback_by_exception: None or dict
"""
def deco_retry(f):
def f_retry(*args, **kwargs):
mtries, mdelay = tries, timeout_secs
run_one_last_time = True
while mtries > 1:
try:
return f(*args, **kwargs)
except ExceptionToCheck as e:
# check if this exception is something the caller wants special handling for
callback_errors = callback_by_exception or {}
for error_type in callback_errors:
if isinstance(e, error_type):
callback_logic = callback_by_exception[error_type]
should_break_out = run_one_last_time = False
if isinstance(callback_logic, (list, tuple)):
callback_logic, should_break_out = callback_logic
if isinstance(should_break_out, (list, tuple)):
should_break_out, run_one_last_time = should_break_out
callback_logic()
if should_break_out: # caller requests we stop handling this exception
break
# traceback.print_exc()
half_interval = mdelay * 0.10 # interval size
actual_delay = random.uniform(mdelay - half_interval, mdelay + half_interval)
msg = "Retrying in %.2f seconds ..." % actual_delay
if logger is None:
logging.exception(msg)
else:
logger.exception(msg)
time.sleep(actual_delay)
mtries -= 1
mdelay *= 2
if run_one_last_time: # one exception may be all the caller wanted in certain cases
return f(*args, **kwargs)
return f_retry # true decorator
return deco_retry
# We re-use `RetryHandler` so that we can reduce duplication; decorator is still useful!
retry_handler = RetryHandler(exc, tries, timeout_secs, logger, callback_by_exception)
return retry_handler


class RetryHandler(object):
"""
Class supporting a more programmatic approach (not requiring a decorator) for retrying logic.
"""
__slots__ = ["exc", "tries", "timeout_secs", "logger", "callback_by_exception"]

def __init__(
self, exc=Exception, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None,
):
self.exc = exc
self.tries = tries
self.timeout_secs = timeout_secs
self.logger = logger
self.callback_by_exception = callback_by_exception
super().__init__()

def __call__(self, f, *args, **kwargs):
retry_return = _deco_retry(
f, self.exc, self.tries, self.timeout_secs, self.logger, self.callback_by_exception,
)
return retry_return
2 changes: 0 additions & 2 deletions setup.cfg

This file was deleted.

6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
long_description=open('README.rst').read() if exists("README.rst") else "",
install_requires=[],
classifiers=[
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
]
)
11 changes: 6 additions & 5 deletions tests/test_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,21 @@ def callback_logic(instance, attr_to_set, value_to_set):
setattr(instance, attr_to_set, value_to_set)


class TestError(Exception):
class ExampleTestError(Exception):
pass


@retry_decorator.retry(ExceptionToCheck=TestError, tries=2, callback_by_exception={
TestError: functools.partial(callback_logic, class_for_testing, 'hello', 'world')})
@retry_decorator.retry(exc=ExampleTestError, tries=2, callback_by_exception={
ExampleTestError: functools.partial(callback_logic, class_for_testing, 'hello', 'world')})
def my_test_func():
raise TestError('oh noes.')
raise ExampleTestError('oh noes.')


@retry_decorator.retry(ExceptionToCheck=(TestError, AttributeError), tries=2, callback_by_exception={
@retry_decorator.retry(exc=(ExampleTestError, AttributeError), tries=2, callback_by_exception={
AttributeError: functools.partial(callback_logic, class_for_testing, 'hello', 'fish')})
def my_test_func_2():
raise AttributeError('attribute oh noes.')


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions tests/test_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def retry_test():
print('hello', file=sys.stderr)
raise Exception('Testing retry')


if __name__ == '__main__':
try:
retry_test()
Expand Down
4 changes: 3 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
distshare={homedir}/.tox/distshare
envlist=py{27,34,35,36}
envlist=py{35,36,37,38}
skip_missing_interpreters=true
indexserver=
pypi = https://pypi.python.org/simple
Expand All @@ -15,3 +15,5 @@ deps =
[pytest]
addopts = -vvl
pep8maxlinelength=120
markers =
pep8

0 comments on commit 8b5c187

Please sign in to comment.