From 79d22269bc9a9fb2c46523cf905d4c0b78958333 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Fri, 7 Jan 2011 18:08:03 +0100 Subject: [PATCH] utils: Split Retry & co. into separate file Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: Iustin Pop <iustin@google.com> --- Makefile.am | 4 +- lib/utils/__init__.py | 157 +----------------------- lib/utils/retry.py | 184 ++++++++++++++++++++++++++++ test/ganeti.utils.retry_unittest.py | 117 ++++++++++++++++++ test/ganeti.utils_unittest.py | 83 ------------- 5 files changed, 305 insertions(+), 240 deletions(-) create mode 100644 lib/utils/retry.py create mode 100755 test/ganeti.utils.retry_unittest.py diff --git a/Makefile.am b/Makefile.am index 76be174de..73ebcfe6c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -213,7 +213,8 @@ server_PYTHON = \ utils_PYTHON = \ lib/utils/__init__.py \ - lib/utils/algo.py + lib/utils/algo.py \ + lib/utils/retry.py docrst = \ doc/admin.rst \ @@ -480,6 +481,7 @@ python_tests = \ test/ganeti.ssh_unittest.py \ test/ganeti.uidpool_unittest.py \ test/ganeti.utils.algo_unittest.py \ + test/ganeti.utils.retry_unittest.py \ test/ganeti.utils_mlockall_unittest.py \ test/ganeti.utils_unittest.py \ test/ganeti.workerpool_unittest.py \ diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index 96db38e6d..1f2e0b7cb 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -63,6 +63,7 @@ from ganeti import constants from ganeti import compat from ganeti.utils.algo import * # pylint: disable-msg=W0401 +from ganeti.utils.retry import * # pylint: disable-msg=W0401 _locksheld = [] _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$') @@ -3353,162 +3354,6 @@ def ReadWatcherPauseFile(filename, now=None, remove_after=3600): return value -class RetryTimeout(Exception): - """Retry loop timed out. - - Any arguments which was passed by the retried function to RetryAgain will be - preserved in RetryTimeout, if it is raised. If such argument was an exception - the RaiseInner helper method will reraise it. - - """ - def RaiseInner(self): - if self.args and isinstance(self.args[0], Exception): - raise self.args[0] - else: - raise RetryTimeout(*self.args) - - -class RetryAgain(Exception): - """Retry again. - - Any arguments passed to RetryAgain will be preserved, if a timeout occurs, as - arguments to RetryTimeout. If an exception is passed, the RaiseInner() method - of the RetryTimeout() method can be used to reraise it. - - """ - - -class _RetryDelayCalculator(object): - """Calculator for increasing delays. - - """ - __slots__ = [ - "_factor", - "_limit", - "_next", - "_start", - ] - - def __init__(self, start, factor, limit): - """Initializes this class. - - @type start: float - @param start: Initial delay - @type factor: float - @param factor: Factor for delay increase - @type limit: float or None - @param limit: Upper limit for delay or None for no limit - - """ - assert start > 0.0 - assert factor >= 1.0 - assert limit is None or limit >= 0.0 - - self._start = start - self._factor = factor - self._limit = limit - - self._next = start - - def __call__(self): - """Returns current delay and calculates the next one. - - """ - current = self._next - - # Update for next run - if self._limit is None or self._next < self._limit: - self._next = min(self._limit, self._next * self._factor) - - return current - - -#: Special delay to specify whole remaining timeout -RETRY_REMAINING_TIME = object() - - -def Retry(fn, delay, timeout, args=None, wait_fn=time.sleep, - _time_fn=time.time): - """Call a function repeatedly until it succeeds. - - The function C{fn} is called repeatedly until it doesn't throw L{RetryAgain} - anymore. Between calls a delay, specified by C{delay}, is inserted. After a - total of C{timeout} seconds, this function throws L{RetryTimeout}. - - C{delay} can be one of the following: - - callable returning the delay length as a float - - Tuple of (start, factor, limit) - - L{RETRY_REMAINING_TIME} to sleep until the timeout expires (this is - useful when overriding L{wait_fn} to wait for an external event) - - A static delay as a number (int or float) - - @type fn: callable - @param fn: Function to be called - @param delay: Either a callable (returning the delay), a tuple of (start, - factor, limit) (see L{_RetryDelayCalculator}), - L{RETRY_REMAINING_TIME} or a number (int or float) - @type timeout: float - @param timeout: Total timeout - @type wait_fn: callable - @param wait_fn: Waiting function - @return: Return value of function - - """ - assert callable(fn) - assert callable(wait_fn) - assert callable(_time_fn) - - if args is None: - args = [] - - end_time = _time_fn() + timeout - - if callable(delay): - # External function to calculate delay - calc_delay = delay - - elif isinstance(delay, (tuple, list)): - # Increasing delay with optional upper boundary - (start, factor, limit) = delay - calc_delay = _RetryDelayCalculator(start, factor, limit) - - elif delay is RETRY_REMAINING_TIME: - # Always use the remaining time - calc_delay = None - - else: - # Static delay - calc_delay = lambda: delay - - assert calc_delay is None or callable(calc_delay) - - while True: - retry_args = [] - try: - # pylint: disable-msg=W0142 - return fn(*args) - except RetryAgain, err: - retry_args = err.args - except RetryTimeout: - raise errors.ProgrammerError("Nested retry loop detected that didn't" - " handle RetryTimeout") - - remaining_time = end_time - _time_fn() - - if remaining_time < 0.0: - # pylint: disable-msg=W0142 - raise RetryTimeout(*retry_args) - - assert remaining_time >= 0.0 - - if calc_delay is None: - wait_fn(remaining_time) - else: - current_delay = calc_delay() - if current_delay > 0.0: - wait_fn(current_delay) - - def GetClosedTempfile(*args, **kwargs): """Creates a temporary file and returns its path. diff --git a/lib/utils/retry.py b/lib/utils/retry.py new file mode 100644 index 000000000..b5b7ff315 --- /dev/null +++ b/lib/utils/retry.py @@ -0,0 +1,184 @@ +# +# + +# Copyright (C) 2006, 2007, 2010, 2011 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +"""Utility functions for retrying function calls with a timeout. + +""" + + +import time + +from ganeti import errors + + +#: Special delay to specify whole remaining timeout +RETRY_REMAINING_TIME = object() + + +class RetryTimeout(Exception): + """Retry loop timed out. + + Any arguments which was passed by the retried function to RetryAgain will be + preserved in RetryTimeout, if it is raised. If such argument was an exception + the RaiseInner helper method will reraise it. + + """ + def RaiseInner(self): + if self.args and isinstance(self.args[0], Exception): + raise self.args[0] + else: + raise RetryTimeout(*self.args) + + +class RetryAgain(Exception): + """Retry again. + + Any arguments passed to RetryAgain will be preserved, if a timeout occurs, as + arguments to RetryTimeout. If an exception is passed, the RaiseInner() method + of the RetryTimeout() method can be used to reraise it. + + """ + + +class _RetryDelayCalculator(object): + """Calculator for increasing delays. + + """ + __slots__ = [ + "_factor", + "_limit", + "_next", + "_start", + ] + + def __init__(self, start, factor, limit): + """Initializes this class. + + @type start: float + @param start: Initial delay + @type factor: float + @param factor: Factor for delay increase + @type limit: float or None + @param limit: Upper limit for delay or None for no limit + + """ + assert start > 0.0 + assert factor >= 1.0 + assert limit is None or limit >= 0.0 + + self._start = start + self._factor = factor + self._limit = limit + + self._next = start + + def __call__(self): + """Returns current delay and calculates the next one. + + """ + current = self._next + + # Update for next run + if self._limit is None or self._next < self._limit: + self._next = min(self._limit, self._next * self._factor) + + return current + + +def Retry(fn, delay, timeout, args=None, wait_fn=time.sleep, + _time_fn=time.time): + """Call a function repeatedly until it succeeds. + + The function C{fn} is called repeatedly until it doesn't throw L{RetryAgain} + anymore. Between calls a delay, specified by C{delay}, is inserted. After a + total of C{timeout} seconds, this function throws L{RetryTimeout}. + + C{delay} can be one of the following: + - callable returning the delay length as a float + - Tuple of (start, factor, limit) + - L{RETRY_REMAINING_TIME} to sleep until the timeout expires (this is + useful when overriding L{wait_fn} to wait for an external event) + - A static delay as a number (int or float) + + @type fn: callable + @param fn: Function to be called + @param delay: Either a callable (returning the delay), a tuple of (start, + factor, limit) (see L{_RetryDelayCalculator}), + L{RETRY_REMAINING_TIME} or a number (int or float) + @type timeout: float + @param timeout: Total timeout + @type wait_fn: callable + @param wait_fn: Waiting function + @return: Return value of function + + """ + assert callable(fn) + assert callable(wait_fn) + assert callable(_time_fn) + + if args is None: + args = [] + + end_time = _time_fn() + timeout + + if callable(delay): + # External function to calculate delay + calc_delay = delay + + elif isinstance(delay, (tuple, list)): + # Increasing delay with optional upper boundary + (start, factor, limit) = delay + calc_delay = _RetryDelayCalculator(start, factor, limit) + + elif delay is RETRY_REMAINING_TIME: + # Always use the remaining time + calc_delay = None + + else: + # Static delay + calc_delay = lambda: delay + + assert calc_delay is None or callable(calc_delay) + + while True: + retry_args = [] + try: + # pylint: disable-msg=W0142 + return fn(*args) + except RetryAgain, err: + retry_args = err.args + except RetryTimeout: + raise errors.ProgrammerError("Nested retry loop detected that didn't" + " handle RetryTimeout") + + remaining_time = end_time - _time_fn() + + if remaining_time < 0.0: + # pylint: disable-msg=W0142 + raise RetryTimeout(*retry_args) + + assert remaining_time >= 0.0 + + if calc_delay is None: + wait_fn(remaining_time) + else: + current_delay = calc_delay() + if current_delay > 0.0: + wait_fn(current_delay) diff --git a/test/ganeti.utils.retry_unittest.py b/test/ganeti.utils.retry_unittest.py new file mode 100755 index 000000000..8173a3045 --- /dev/null +++ b/test/ganeti.utils.retry_unittest.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# + +# Copyright (C) 2011 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Script for testing ganeti.utils.retry""" + +import unittest + +from ganeti import constants +from ganeti import errors +from ganeti import utils + +import testutils + + +class TestRetry(testutils.GanetiTestCase): + def setUp(self): + testutils.GanetiTestCase.setUp(self) + self.retries = 0 + + @staticmethod + def _RaiseRetryAgain(): + raise utils.RetryAgain() + + @staticmethod + def _RaiseRetryAgainWithArg(args): + raise utils.RetryAgain(*args) + + def _WrongNestedLoop(self): + return utils.Retry(self._RaiseRetryAgain, 0.01, 0.02) + + def _RetryAndSucceed(self, retries): + if self.retries < retries: + self.retries += 1 + raise utils.RetryAgain() + else: + return True + + def testRaiseTimeout(self): + self.failUnlessRaises(utils.RetryTimeout, utils.Retry, + self._RaiseRetryAgain, 0.01, 0.02) + self.failUnlessRaises(utils.RetryTimeout, utils.Retry, + self._RetryAndSucceed, 0.01, 0, args=[1]) + self.failUnlessEqual(self.retries, 1) + + def testComplete(self): + self.failUnlessEqual(utils.Retry(lambda: True, 0, 1), True) + self.failUnlessEqual(utils.Retry(self._RetryAndSucceed, 0, 1, args=[2]), + True) + self.failUnlessEqual(self.retries, 2) + + def testNestedLoop(self): + try: + self.failUnlessRaises(errors.ProgrammerError, utils.Retry, + self._WrongNestedLoop, 0, 1) + except utils.RetryTimeout: + self.fail("Didn't detect inner loop's exception") + + def testTimeoutArgument(self): + retry_arg="my_important_debugging_message" + try: + utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, args=[[retry_arg]]) + except utils.RetryTimeout, err: + self.failUnlessEqual(err.args, (retry_arg, )) + else: + self.fail("Expected timeout didn't happen") + + def testRaiseInnerWithExc(self): + retry_arg="my_important_debugging_message" + try: + try: + utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, + args=[[errors.GenericError(retry_arg, retry_arg)]]) + except utils.RetryTimeout, err: + err.RaiseInner() + else: + self.fail("Expected timeout didn't happen") + except errors.GenericError, err: + self.failUnlessEqual(err.args, (retry_arg, retry_arg)) + else: + self.fail("Expected GenericError didn't happen") + + def testRaiseInnerWithMsg(self): + retry_arg="my_important_debugging_message" + try: + try: + utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, + args=[[retry_arg, retry_arg]]) + except utils.RetryTimeout, err: + err.RaiseInner() + else: + self.fail("Expected timeout didn't happen") + except utils.RetryTimeout, err: + self.failUnlessEqual(err.args, (retry_arg, retry_arg)) + else: + self.fail("Expected RetryTimeout didn't happen") + + +if __name__ == "__main__": + testutils.GanetiTestProgram() diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index 254c6d87a..24deb078a 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -1995,89 +1995,6 @@ class TestMakedirs(unittest.TestCase): self.assert_(os.path.isdir(path)) -class TestRetry(testutils.GanetiTestCase): - def setUp(self): - testutils.GanetiTestCase.setUp(self) - self.retries = 0 - - @staticmethod - def _RaiseRetryAgain(): - raise utils.RetryAgain() - - @staticmethod - def _RaiseRetryAgainWithArg(args): - raise utils.RetryAgain(*args) - - def _WrongNestedLoop(self): - return utils.Retry(self._RaiseRetryAgain, 0.01, 0.02) - - def _RetryAndSucceed(self, retries): - if self.retries < retries: - self.retries += 1 - raise utils.RetryAgain() - else: - return True - - def testRaiseTimeout(self): - self.failUnlessRaises(utils.RetryTimeout, utils.Retry, - self._RaiseRetryAgain, 0.01, 0.02) - self.failUnlessRaises(utils.RetryTimeout, utils.Retry, - self._RetryAndSucceed, 0.01, 0, args=[1]) - self.failUnlessEqual(self.retries, 1) - - def testComplete(self): - self.failUnlessEqual(utils.Retry(lambda: True, 0, 1), True) - self.failUnlessEqual(utils.Retry(self._RetryAndSucceed, 0, 1, args=[2]), - True) - self.failUnlessEqual(self.retries, 2) - - def testNestedLoop(self): - try: - self.failUnlessRaises(errors.ProgrammerError, utils.Retry, - self._WrongNestedLoop, 0, 1) - except utils.RetryTimeout: - self.fail("Didn't detect inner loop's exception") - - def testTimeoutArgument(self): - retry_arg="my_important_debugging_message" - try: - utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, args=[[retry_arg]]) - except utils.RetryTimeout, err: - self.failUnlessEqual(err.args, (retry_arg, )) - else: - self.fail("Expected timeout didn't happen") - - def testRaiseInnerWithExc(self): - retry_arg="my_important_debugging_message" - try: - try: - utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, - args=[[errors.GenericError(retry_arg, retry_arg)]]) - except utils.RetryTimeout, err: - err.RaiseInner() - else: - self.fail("Expected timeout didn't happen") - except errors.GenericError, err: - self.failUnlessEqual(err.args, (retry_arg, retry_arg)) - else: - self.fail("Expected GenericError didn't happen") - - def testRaiseInnerWithMsg(self): - retry_arg="my_important_debugging_message" - try: - try: - utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, - args=[[retry_arg, retry_arg]]) - except utils.RetryTimeout, err: - err.RaiseInner() - else: - self.fail("Expected timeout didn't happen") - except utils.RetryTimeout, err: - self.failUnlessEqual(err.args, (retry_arg, retry_arg)) - else: - self.fail("Expected RetryTimeout didn't happen") - - class TestLineSplitter(unittest.TestCase): def test(self): lines = [] -- GitLab