Bug 1118774 - Import python redo library; r=gps
authorMike Shal <mshal@mozilla.com>
Wed, 07 Jan 2015 14:18:20 -0500
changeset 249199 a1654fa1847df3b85fc1d83879c9290332ea0325
parent 249198 ac3b15d066657f307b8bcad8bd1b9993f0f82ece
child 249200 fff1eae2346fce938e64f070280a1528330a769c
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1118774
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1118774 - Import python redo library; r=gps
build/virtualenv_packages.txt
python/redo/PKG-INFO
python/redo/README
python/redo/redo.egg-info/PKG-INFO
python/redo/redo.egg-info/SOURCES.txt
python/redo/redo.egg-info/dependency_links.txt
python/redo/redo.egg-info/entry_points.txt
python/redo/redo.egg-info/top_level.txt
python/redo/redo/__init__.py
python/redo/redo/cmd.py
python/redo/setup.cfg
python/redo/setup.py
--- a/build/virtualenv_packages.txt
+++ b/build/virtualenv_packages.txt
@@ -19,8 +19,9 @@ mozilla.pth:dom/bindings/parser
 mozilla.pth:layout/tools/reftest
 moztreedocs.pth:tools/docs
 copy:build/buildconfig.py
 packages.txt:testing/mozbase/packages.txt
 objdir:build
 gyp.pth:media/webrtc/trunk/tools/gyp/pylib
 pyasn1.pth:python/pyasn1
 bitstring.pth:python/bitstring
+redo.pth:python/redo
new file mode 100644
--- /dev/null
+++ b/python/redo/PKG-INFO
@@ -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
new file mode 100644
--- /dev/null
+++ b/python/redo/README
@@ -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.
new file mode 100644
--- /dev/null
+++ b/python/redo/redo.egg-info/PKG-INFO
@@ -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
new file mode 100644
--- /dev/null
+++ b/python/redo/redo.egg-info/SOURCES.txt
@@ -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
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/python/redo/redo.egg-info/dependency_links.txt
@@ -0,0 +1,1 @@
+
new file mode 100644
--- /dev/null
+++ b/python/redo/redo.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+retry = redo.cmd:main
+
new file mode 100644
--- /dev/null
+++ b/python/redo/redo.egg-info/top_level.txt
@@ -0,0 +1,1 @@
+redo
new file mode 100644
--- /dev/null
+++ b/python/redo/redo/__init__.py
@@ -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)
new file mode 100644
--- /dev/null
+++ b/python/redo/redo/cmd.py
@@ -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()
new file mode 100644
--- /dev/null
+++ b/python/redo/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
new file mode 100644
--- /dev/null
+++ b/python/redo/setup.py
@@ -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",
+)