Bug 1118774 - Import python redo library; r=gps

This commit is contained in:
Mike Shal
2015-01-07 14:18:20 -05:00
parent 413f14eccf
commit bc11b40e5d
12 changed files with 329 additions and 0 deletions

View File

@@ -24,3 +24,4 @@ objdir:build
gyp.pth:media/webrtc/trunk/tools/gyp/pylib gyp.pth:media/webrtc/trunk/tools/gyp/pylib
pyasn1.pth:python/pyasn1 pyasn1.pth:python/pyasn1
bitstring.pth:python/bitstring bitstring.pth:python/bitstring
redo.pth:python/redo

10
python/redo/PKG-INFO Normal file
View File

@@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: redo
Version: 1.4
Summary: Utilities to retry Python callables.
Home-page: https://github.com/bhearsum/redo
Author: Ben Hearsum
Author-email: ben@hearsum.ca
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN

4
python/redo/README Normal file
View File

@@ -0,0 +1,4 @@
Redo - Utilities to retry Python callables
******************************************
Redo provides various means to add seamless retriability to any Python callable. Redo includes a plain function (redo.retry), a decorator (redo.retriable), and a context manager (redo.retrying) to enable you to integrate it in the best possible way for your project. As a bonus, a standalone interface is also included ("retry"). For details and sample invocations have a look at the docstrings in redo/__init__.py.

View File

@@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: redo
Version: 1.4
Summary: Utilities to retry Python callables.
Home-page: https://github.com/bhearsum/redo
Author: Ben Hearsum
Author-email: ben@hearsum.ca
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN

View File

@@ -0,0 +1,9 @@
README
setup.py
redo/__init__.py
redo/cmd.py
redo.egg-info/PKG-INFO
redo.egg-info/SOURCES.txt
redo.egg-info/dependency_links.txt
redo.egg-info/entry_points.txt
redo.egg-info/top_level.txt

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
[console_scripts]
retry = redo.cmd:main

View File

@@ -0,0 +1 @@
redo

View File

@@ -0,0 +1,218 @@
# ***** BEGIN LICENSE BLOCK *****
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
# ***** END LICENSE BLOCK *****
import time
from functools import wraps
from contextlib import contextmanager
import logging
import random
log = logging.getLogger(__name__)
def retrier(attempts=5, sleeptime=10, max_sleeptime=300, sleepscale=1.5, jitter=1):
"""
A generator function that sleeps between retries, handles exponential
backoff and jitter. The action you are retrying is meant to run after
retrier yields.
At each iteration, we sleep for sleeptime + random.randint(-jitter, jitter).
Afterwards sleeptime is multiplied by sleepscale for the next iteration.
Args:
attempts (int): maximum number of times to try; defaults to 5
sleeptime (float): how many seconds to sleep between tries; defaults to
60s (one minute)
max_sleeptime (float): the longest we'll sleep, in seconds; defaults to
300s (five minutes)
sleepscale (float): how much to multiply the sleep time by each
iteration; defaults to 1.5
jitter (int): random jitter to introduce to sleep time each iteration.
the amount is chosen at random between [-jitter, +jitter]
defaults to 1
Yields:
None, a maximum of `attempts` number of times
Example:
>>> n = 0
>>> for _ in retrier(sleeptime=0, jitter=0):
... if n == 3:
... # We did the thing!
... break
... n += 1
>>> n
3
>>> n = 0
>>> for _ in retrier(sleeptime=0, jitter=0):
... if n == 6:
... # We did the thing!
... break
... n += 1
... else:
... print "max tries hit"
max tries hit
"""
for _ in range(attempts):
log.debug("attempt %i/%i", _ + 1, attempts)
yield
if jitter:
sleeptime += random.randint(-jitter, jitter)
sleeptime = max(sleeptime, 0)
if _ == attempts - 1:
# Don't need to sleep the last time
break
log.debug("sleeping for %.2fs (attempt %i/%i)", sleeptime, _ + 1, attempts)
time.sleep(sleeptime)
sleeptime *= sleepscale
if sleeptime > max_sleeptime:
sleeptime = max_sleeptime
def retry(action, attempts=5, sleeptime=60, max_sleeptime=5 * 60,
sleepscale=1.5, jitter=1, retry_exceptions=(Exception,),
cleanup=None, args=(), kwargs={}):
"""
Calls an action function until it succeeds, or we give up.
Args:
action (callable): the function to retry
attempts (int): maximum number of times to try; defaults to 5
sleeptime (float): how many seconds to sleep between tries; defaults to
60s (one minute)
max_sleeptime (float): the longest we'll sleep, in seconds; defaults to
300s (five minutes)
sleepscale (float): how much to multiply the sleep time by each
iteration; defaults to 1.5
jitter (int): random jitter to introduce to sleep time each iteration.
the amount is chosen at random between [-jitter, +jitter]
defaults to 1
retry_exceptions (tuple): tuple of exceptions to be caught. If other
exceptions are raised by action(), then these
are immediately re-raised to the caller.
cleanup (callable): optional; called if one of `retry_exceptions` is
caught. No arguments are passed to the cleanup
function; if your cleanup requires arguments,
consider using functools.partial or a lambda
function.
args (tuple): positional arguments to call `action` with
hwargs (dict): keyword arguments to call `action` with
Returns:
Whatever action(*args, **kwargs) returns
Raises:
Whatever action(*args, **kwargs) raises. `retry_exceptions` are caught
up until the last attempt, in which case they are re-raised.
Example:
>>> count = 0
>>> def foo():
... global count
... count += 1
... print count
... if count < 3:
... raise ValueError("count is too small!")
... return "success!"
>>> retry(foo, sleeptime=0, jitter=0)
1
2
3
'success!'
"""
assert callable(action)
assert not cleanup or callable(cleanup)
if max_sleeptime < sleeptime:
log.debug("max_sleeptime %d less than sleeptime %d" % (
max_sleeptime, sleeptime))
n = 1
for _ in retrier(attempts=attempts, sleeptime=sleeptime,
max_sleeptime=max_sleeptime, sleepscale=sleepscale,
jitter=jitter):
try:
log.info("retry: Calling %s with args: %s, kwargs: %s, "
"attempt #%d" % (action, str(args), str(kwargs), n))
return action(*args, **kwargs)
except retry_exceptions:
log.debug("retry: Caught exception: ", exc_info=True)
if cleanup:
cleanup()
if n == attempts:
log.info("retry: Giving up on %s" % action)
raise
continue
finally:
n += 1
def retriable(*retry_args, **retry_kwargs):
"""
A decorator factory for retry(). Wrap your function in @retriable(...) to
give it retry powers!
Arguments:
Same as for `retry`, with the exception of `action`, `args`, and `kwargs`,
which are left to the normal function definition.
Returns:
A function decorator
Example:
>>> count = 0
>>> @retriable(sleeptime=0, jitter=0)
... def foo():
... global count
... count += 1
... print count
... if count < 3:
... raise ValueError("count too small")
... return "success!"
>>> foo()
1
2
3
'success!'
"""
def _retriable_factory(func):
@wraps(func)
def _retriable_wrapper(*args, **kwargs):
return retry(func, args=args, kwargs=kwargs, *retry_args,
**retry_kwargs)
return _retriable_wrapper
return _retriable_factory
@contextmanager
def retrying(func, *retry_args, **retry_kwargs):
"""
A context manager for wrapping functions with retry functionality.
Arguments:
func (callable): the function to wrap
other arguments as per `retry`
Returns:
A context manager that returns retriable(func) on __enter__
Example:
>>> count = 0
>>> def foo():
... global count
... count += 1
... print count
... if count < 3:
... raise ValueError("count too small")
... return "success!"
>>> with retrying(foo, sleeptime=0, jitter=0) as f:
... f()
1
2
3
'success!'
"""
yield retriable(*retry_args, **retry_kwargs)(func)

53
python/redo/redo/cmd.py Normal file
View File

@@ -0,0 +1,53 @@
# ***** BEGIN LICENSE BLOCK *****
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
# ***** END LICENSE BLOCK *****
import logging
from subprocess import check_call, CalledProcessError
import sys
from redo import retrying
log = logging.getLogger(__name__)
def main():
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument(
"-a", "--attempts", type=int, default=5,
help="How many times to retry.")
parser.add_argument(
"-s", "--sleeptime", type=int, default=60,
help="How long to sleep between attempts. Sleeptime doubles after each attempt.")
parser.add_argument(
"-m", "--max-sleeptime", type=int, default=5*60,
help="Maximum length of time to sleep between attempts (limits backoff length).")
parser.add_argument("-v", "--verbose", action="store_true", default=False)
parser.add_argument("cmd", nargs="+", help="Command to run. Eg: wget http://blah")
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.INFO)
logging.getLogger("retry").setLevel(logging.INFO)
else:
logging.basicConfig(level=logging.ERROR)
logging.getLogger("retry").setLevel(logging.ERROR)
try:
with retrying(check_call, attempts=args.attempts, sleeptime=args.sleeptime,
max_sleeptime=args.max_sleeptime,
retry_exceptions=(CalledProcessError,)) as r_check_call:
r_check_call(args.cmd)
except KeyboardInterrupt:
sys.exit(-1)
except Exception as e:
log.error("Unable to run command after %d attempts" % args.attempts, exc_info=True)
rc = getattr(e, "returncode", -2)
sys.exit(rc)
if __name__ == "__main__":
main()

5
python/redo/setup.cfg Normal file
View File

@@ -0,0 +1,5 @@
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

14
python/redo/setup.py Normal file
View File

@@ -0,0 +1,14 @@
from setuptools import setup
setup(
name="redo",
version="1.4",
description="Utilities to retry Python callables.",
author="Ben Hearsum",
author_email="ben@hearsum.ca",
packages=["redo"],
entry_points={
"console_scripts": ["retry = redo.cmd:main"],
},
url="https://github.com/bhearsum/redo",
)