blob: 807930321388538b4942e3e35ff49c8b308a3abb [file] [log] [blame]
#
#
# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Utility functions for retrying function calls with a timeout.
"""
import logging
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=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=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 SimpleRetry(expected, fn, delay, timeout, args=None, wait_fn=time.sleep,
_time_fn=time.time):
"""A wrapper over L{Retry} implementing a simpler interface.
All the parameters are the same as for L{Retry}, except it has one
extra argument: expected, which can be either a value (will be
compared with the result of the function, or a callable (which will
get the result passed and has to return a boolean). If the test is
false, we will retry until either the timeout has passed or the
tests succeeds. In both cases, the last result from calling the
function will be returned.
Note that this function is not expected to raise any retry-related
exceptions, always simply returning values. As such, the function is
designed to allow easy wrapping of code that doesn't use retry at
all (e.g. "if fn(args)" replaced with "if SimpleRetry(True, fn,
...)".
@see: L{Retry}
"""
rdict = {}
def helper(*innerargs):
# pylint: disable=W0142
result = rdict["result"] = fn(*innerargs)
if not ((callable(expected) and expected(result)) or result == expected):
raise RetryAgain()
return result
try:
result = Retry(helper, delay, timeout, args=args,
wait_fn=wait_fn, _time_fn=_time_fn)
except RetryTimeout:
assert "result" in rdict
result = rdict["result"]
return result
def CountRetry(expected, fn, count, args=None):
"""A wrapper over L{SimpleRetry} implementing a count down.
Where L{Retry} fixes the time, after which the command is assumed to be
failing, this function assumes the total number of tries.
@see: L{Retry}
"""
rdict = {"tries": 0}
get_tries = lambda: rdict["tries"]
def inc_tries(t):
rdict["tries"] += t
return SimpleRetry(expected, fn, 1, count, args=args,
wait_fn=inc_tries, _time_fn=get_tries)
def RetryByNumberOfTimes(max_retries, exception_class, fn, *args, **kwargs):
"""Retries calling a function up to the specified number of times.
@type max_retries: integer
@param max_retries: Maximum number of retries.
@type exception_class: class
@param exception_class: Exception class which is used for throwing the
final exception.
@type fn: callable
@param fn: Function to be called (up to the specified maximum number of
retries.
"""
last_exception = None
for i in range(max_retries):
try:
fn(*args, **kwargs)
break
except errors.OpExecError as e:
logging.error("Error after retry no. %s: %s.", i, e)
last_exception = e
else:
if last_exception:
raise exception_class("Error after %s retries. Last exception: %s."
% (max_retries, last_exception))