Bug 1118774 - Import python redo library; r=gps
authorMike Shal <mshal@mozilla.com>
Wed, 07 Jan 2015 14:18:20 -0500
changeset 240111 a1654fa1847df3b85fc1d83879c9290332ea0325
parent 240110 ac3b15d066657f307b8bcad8bd1b9993f0f82ece
child 240112 fff1eae2346fce938e64f070280a1528330a769c
push id7472
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 20:36:27 +0000
treeherdermozilla-aurora@300ca104f8fb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1118774
milestone37.0a1
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",
+)