Skip to content
This repository was archived by the owner on Aug 8, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,37 @@ backoff behavior for different cases::
def poll_for_message(queue):
return queue.get()

Runtime Configuration
---------------------

The decorator functions ``on_exception`` and ``on_predicate`` are
generally evaluated at import time. This is fine when the keyword args
are passed as constant values, but suppose we want to consult a
dictionary with configuration options that only become available at
runtime. The relevant values are not available at import time. Instead,
decorator functions can be passed callables which are evaluated at
runtime to obtain the value::

def lookup_max_tries():
# pretend we have a global reference to 'app' here
# and that it has a dictionary-like 'config' property
return app.config["BACKOFF_MAX_TRIES"]

@backoff.on_exception(backoff.expo,
ValueError,
max_tries=lookup_max_tries)

More cleverly, you might define a function which returns a lookup
function for a specified variable::

def config(app, name):
return functools.partial(app.config.get, name)

@backoff.on_exception(backoff.expo,
ValueError,
max_value=config(app, "BACKOFF_MAX_VALUE")
max_tries=config(app, "BACKOFF_MAX_TRIES"))

Event handlers
--------------

Expand Down
39 changes: 29 additions & 10 deletions backoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ def on_predicate(wait_gen,
is exceeded. The parameter is a dict containing details
about the invocation.
**wait_gen_kwargs: Any additional keyword args specified will be
passed to wait_gen when it is initialized.
passed to wait_gen when it is initialized. Any callable
args will first be evaluated and their return values passed.
This is useful for runtime configuration.
"""
success_hdlrs = _handlers(on_success)
backoff_hdlrs = _handlers(on_backoff, _log_backoff)
Expand All @@ -158,14 +160,19 @@ def decorate(target):

@functools.wraps(target)
def retry(*args, **kwargs):
tries = 0
# change names because python 2.x doesn't have nonlocal
max_tries_ = _maybe_call(max_tries)

# there are no dictionary comprehensions in python 2.6
wait = wait_gen(**dict((k, _maybe_call(v))
for k, v in wait_gen_kwargs.items()))

wait = wait_gen(**wait_gen_kwargs)
tries = 0
while True:
tries += 1
ret = target(*args, **kwargs)
if predicate(ret):
if tries == max_tries:
if tries == max_tries_:
for hdlr in giveup_hdlrs:
hdlr({'target': target,
'args': args,
Expand Down Expand Up @@ -231,7 +238,8 @@ def on_exception(wait_gen,
max_tries: The maximum number of attempts to make before giving
up. Once exhausted, the exception will be allowed to escape.
The default value of None means their is no limit to the
number of tries.
number of tries. If a callable is passed, it will be
evaluated at runtime and its return value used.
jitter: A function of the value yielded by wait_gen returning
the actual time to wait. This distributes wait times
stochastically in order to avoid timing collisions across
Expand All @@ -252,8 +260,9 @@ def on_exception(wait_gen,
is exceeded. The parameter is a dict containing details
about the invocation.
**wait_gen_kwargs: Any additional keyword args specified will be
passed to wait_gen when it is initialized.

passed to wait_gen when it is initialized. Any callable
args will first be evaluated and their return values passed.
This is useful for runtime configuration.
"""
success_hdlrs = _handlers(on_success)
backoff_hdlrs = _handlers(on_backoff, _log_backoff)
Expand All @@ -263,15 +272,20 @@ def decorate(target):

@functools.wraps(target)
def retry(*args, **kwargs):
# change names because python 2.x doesn't have nonlocal
max_tries_ = _maybe_call(max_tries)

# there are no dictionary comprehensions in python 2.6
wait = wait_gen(**dict((k, _maybe_call(v))
for k, v in wait_gen_kwargs.items()))

tries = 0
wait = wait_gen(**wait_gen_kwargs)
while True:
try:
tries += 1
ret = target(*args, **kwargs)
except exception as e:
if giveup(e) or tries == max_tries:

if giveup(e) or tries == max_tries_:
for hdlr in giveup_hdlrs:
hdlr({'target': target,
'args': args,
Expand Down Expand Up @@ -326,6 +340,11 @@ def _handlers(hdlr, default=None):
return defaults + [hdlr]


# Evaluate arg that can be either a fixed value or a callable.
def _maybe_call(f, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would prefer to not rely on exceptions for processing and do something along the lines of http://stackoverflow.com/questions/624926/how-to-detect-whether-a-python-variable-is-a-function

Copy link
Member Author

@bgreen-litl bgreen-litl Aug 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression there wasn't a portable way to do it across 2.6 (we've got some 2.6 PRs so I'm trying to keep supporting this) 2.7 3.x but it looks like the callable built-in should work despite briefly going away in early 3.x. I added a fixup to use it.

return f(*args, **kwargs) if callable(f) else f


# Formats a function invocation as a unicode string for logging.
def _invoc_repr(details):
f, args, kwargs = details['target'], details['args'], details['kwargs']
Expand Down
45 changes: 45 additions & 0 deletions backoff_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,3 +516,48 @@ def success(*args, **kwargs):
'target': success._target,
'tries': 3,
'value': True}


def test_on_exception_callable_max_tries(monkeypatch):
monkeypatch.setattr('time.sleep', lambda x: None)

def lookup_max_tries():
return 3

log = []

@backoff.on_exception(backoff.constant,
ValueError,
max_tries=lookup_max_tries)
def exceptor():
log.append(True)
raise ValueError()

with pytest.raises(ValueError):
exceptor()

assert len(log) == 3


def test_on_exception_callable_gen_kwargs():

def lookup_foo():
return "foo"

def wait_gen(foo=None, bar=None):
assert foo == "foo"
assert bar == "bar"

while True:
yield 0

@backoff.on_exception(wait_gen,
ValueError,
max_tries=2,
foo=lookup_foo,
bar="bar")
def exceptor():
raise ValueError("aah")

with pytest.raises(ValueError):
exceptor()