Skip to content
This repository was archived by the owner on Aug 8, 2025. It is now read-only.

Commit 0127223

Browse files
authored
Merge pull request #12 from bgreen-litl/callable-kwargs
Support runtime configuration with optionally callable kwargs
2 parents e105de7 + ce9d114 commit 0127223

File tree

3 files changed

+105
-10
lines changed

3 files changed

+105
-10
lines changed

README.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,37 @@ backoff behavior for different cases::
128128
def poll_for_message(queue):
129129
return queue.get()
130130

131+
Runtime Configuration
132+
---------------------
133+
134+
The decorator functions ``on_exception`` and ``on_predicate`` are
135+
generally evaluated at import time. This is fine when the keyword args
136+
are passed as constant values, but suppose we want to consult a
137+
dictionary with configuration options that only become available at
138+
runtime. The relevant values are not available at import time. Instead,
139+
decorator functions can be passed callables which are evaluated at
140+
runtime to obtain the value::
141+
142+
def lookup_max_tries():
143+
# pretend we have a global reference to 'app' here
144+
# and that it has a dictionary-like 'config' property
145+
return app.config["BACKOFF_MAX_TRIES"]
146+
147+
@backoff.on_exception(backoff.expo,
148+
ValueError,
149+
max_tries=lookup_max_tries)
150+
151+
More cleverly, you might define a function which returns a lookup
152+
function for a specified variable::
153+
154+
def config(app, name):
155+
return functools.partial(app.config.get, name)
156+
157+
@backoff.on_exception(backoff.expo,
158+
ValueError,
159+
max_value=config(app, "BACKOFF_MAX_VALUE")
160+
max_tries=config(app, "BACKOFF_MAX_TRIES"))
161+
131162
Event handlers
132163
--------------
133164

backoff.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ def on_predicate(wait_gen,
148148
is exceeded. The parameter is a dict containing details
149149
about the invocation.
150150
**wait_gen_kwargs: Any additional keyword args specified will be
151-
passed to wait_gen when it is initialized.
151+
passed to wait_gen when it is initialized. Any callable
152+
args will first be evaluated and their return values passed.
153+
This is useful for runtime configuration.
152154
"""
153155
success_hdlrs = _handlers(on_success)
154156
backoff_hdlrs = _handlers(on_backoff, _log_backoff)
@@ -158,14 +160,19 @@ def decorate(target):
158160

159161
@functools.wraps(target)
160162
def retry(*args, **kwargs):
161-
tries = 0
163+
# change names because python 2.x doesn't have nonlocal
164+
max_tries_ = _maybe_call(max_tries)
165+
166+
# there are no dictionary comprehensions in python 2.6
167+
wait = wait_gen(**dict((k, _maybe_call(v))
168+
for k, v in wait_gen_kwargs.items()))
162169

163-
wait = wait_gen(**wait_gen_kwargs)
170+
tries = 0
164171
while True:
165172
tries += 1
166173
ret = target(*args, **kwargs)
167174
if predicate(ret):
168-
if tries == max_tries:
175+
if tries == max_tries_:
169176
for hdlr in giveup_hdlrs:
170177
hdlr({'target': target,
171178
'args': args,
@@ -231,7 +238,8 @@ def on_exception(wait_gen,
231238
max_tries: The maximum number of attempts to make before giving
232239
up. Once exhausted, the exception will be allowed to escape.
233240
The default value of None means their is no limit to the
234-
number of tries.
241+
number of tries. If a callable is passed, it will be
242+
evaluated at runtime and its return value used.
235243
jitter: A function of the value yielded by wait_gen returning
236244
the actual time to wait. This distributes wait times
237245
stochastically in order to avoid timing collisions across
@@ -252,8 +260,9 @@ def on_exception(wait_gen,
252260
is exceeded. The parameter is a dict containing details
253261
about the invocation.
254262
**wait_gen_kwargs: Any additional keyword args specified will be
255-
passed to wait_gen when it is initialized.
256-
263+
passed to wait_gen when it is initialized. Any callable
264+
args will first be evaluated and their return values passed.
265+
This is useful for runtime configuration.
257266
"""
258267
success_hdlrs = _handlers(on_success)
259268
backoff_hdlrs = _handlers(on_backoff, _log_backoff)
@@ -263,15 +272,20 @@ def decorate(target):
263272

264273
@functools.wraps(target)
265274
def retry(*args, **kwargs):
275+
# change names because python 2.x doesn't have nonlocal
276+
max_tries_ = _maybe_call(max_tries)
277+
278+
# there are no dictionary comprehensions in python 2.6
279+
wait = wait_gen(**dict((k, _maybe_call(v))
280+
for k, v in wait_gen_kwargs.items()))
281+
266282
tries = 0
267-
wait = wait_gen(**wait_gen_kwargs)
268283
while True:
269284
try:
270285
tries += 1
271286
ret = target(*args, **kwargs)
272287
except exception as e:
273-
if giveup(e) or tries == max_tries:
274-
288+
if giveup(e) or tries == max_tries_:
275289
for hdlr in giveup_hdlrs:
276290
hdlr({'target': target,
277291
'args': args,
@@ -326,6 +340,11 @@ def _handlers(hdlr, default=None):
326340
return defaults + [hdlr]
327341

328342

343+
# Evaluate arg that can be either a fixed value or a callable.
344+
def _maybe_call(f, *args, **kwargs):
345+
return f(*args, **kwargs) if callable(f) else f
346+
347+
329348
# Formats a function invocation as a unicode string for logging.
330349
def _invoc_repr(details):
331350
f, args, kwargs = details['target'], details['args'], details['kwargs']

backoff_tests.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,3 +516,48 @@ def success(*args, **kwargs):
516516
'target': success._target,
517517
'tries': 3,
518518
'value': True}
519+
520+
521+
def test_on_exception_callable_max_tries(monkeypatch):
522+
monkeypatch.setattr('time.sleep', lambda x: None)
523+
524+
def lookup_max_tries():
525+
return 3
526+
527+
log = []
528+
529+
@backoff.on_exception(backoff.constant,
530+
ValueError,
531+
max_tries=lookup_max_tries)
532+
def exceptor():
533+
log.append(True)
534+
raise ValueError()
535+
536+
with pytest.raises(ValueError):
537+
exceptor()
538+
539+
assert len(log) == 3
540+
541+
542+
def test_on_exception_callable_gen_kwargs():
543+
544+
def lookup_foo():
545+
return "foo"
546+
547+
def wait_gen(foo=None, bar=None):
548+
assert foo == "foo"
549+
assert bar == "bar"
550+
551+
while True:
552+
yield 0
553+
554+
@backoff.on_exception(wait_gen,
555+
ValueError,
556+
max_tries=2,
557+
foo=lookup_foo,
558+
bar="bar")
559+
def exceptor():
560+
raise ValueError("aah")
561+
562+
with pytest.raises(ValueError):
563+
exceptor()

0 commit comments

Comments
 (0)