Revision 79d22269

b/Makefile.am
213 213

  
214 214
utils_PYTHON = \
215 215
	lib/utils/__init__.py \
216
	lib/utils/algo.py
216
	lib/utils/algo.py \
217
	lib/utils/retry.py
217 218

  
218 219
docrst = \
219 220
	doc/admin.rst \
......
480 481
	test/ganeti.ssh_unittest.py \
481 482
	test/ganeti.uidpool_unittest.py \
482 483
	test/ganeti.utils.algo_unittest.py \
484
	test/ganeti.utils.retry_unittest.py \
483 485
	test/ganeti.utils_mlockall_unittest.py \
484 486
	test/ganeti.utils_unittest.py \
485 487
	test/ganeti.workerpool_unittest.py \
b/lib/utils/__init__.py
63 63
from ganeti import compat
64 64

  
65 65
from ganeti.utils.algo import * # pylint: disable-msg=W0401
66
from ganeti.utils.retry import * # pylint: disable-msg=W0401
66 67

  
67 68
_locksheld = []
68 69
_re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
......
3353 3354
  return value
3354 3355

  
3355 3356

  
3356
class RetryTimeout(Exception):
3357
  """Retry loop timed out.
3358

  
3359
  Any arguments which was passed by the retried function to RetryAgain will be
3360
  preserved in RetryTimeout, if it is raised. If such argument was an exception
3361
  the RaiseInner helper method will reraise it.
3362

  
3363
  """
3364
  def RaiseInner(self):
3365
    if self.args and isinstance(self.args[0], Exception):
3366
      raise self.args[0]
3367
    else:
3368
      raise RetryTimeout(*self.args)
3369

  
3370

  
3371
class RetryAgain(Exception):
3372
  """Retry again.
3373

  
3374
  Any arguments passed to RetryAgain will be preserved, if a timeout occurs, as
3375
  arguments to RetryTimeout. If an exception is passed, the RaiseInner() method
3376
  of the RetryTimeout() method can be used to reraise it.
3377

  
3378
  """
3379

  
3380

  
3381
class _RetryDelayCalculator(object):
3382
  """Calculator for increasing delays.
3383

  
3384
  """
3385
  __slots__ = [
3386
    "_factor",
3387
    "_limit",
3388
    "_next",
3389
    "_start",
3390
    ]
3391

  
3392
  def __init__(self, start, factor, limit):
3393
    """Initializes this class.
3394

  
3395
    @type start: float
3396
    @param start: Initial delay
3397
    @type factor: float
3398
    @param factor: Factor for delay increase
3399
    @type limit: float or None
3400
    @param limit: Upper limit for delay or None for no limit
3401

  
3402
    """
3403
    assert start > 0.0
3404
    assert factor >= 1.0
3405
    assert limit is None or limit >= 0.0
3406

  
3407
    self._start = start
3408
    self._factor = factor
3409
    self._limit = limit
3410

  
3411
    self._next = start
3412

  
3413
  def __call__(self):
3414
    """Returns current delay and calculates the next one.
3415

  
3416
    """
3417
    current = self._next
3418

  
3419
    # Update for next run
3420
    if self._limit is None or self._next < self._limit:
3421
      self._next = min(self._limit, self._next * self._factor)
3422

  
3423
    return current
3424

  
3425

  
3426
#: Special delay to specify whole remaining timeout
3427
RETRY_REMAINING_TIME = object()
3428

  
3429

  
3430
def Retry(fn, delay, timeout, args=None, wait_fn=time.sleep,
3431
          _time_fn=time.time):
3432
  """Call a function repeatedly until it succeeds.
3433

  
3434
  The function C{fn} is called repeatedly until it doesn't throw L{RetryAgain}
3435
  anymore. Between calls a delay, specified by C{delay}, is inserted. After a
3436
  total of C{timeout} seconds, this function throws L{RetryTimeout}.
3437

  
3438
  C{delay} can be one of the following:
3439
    - callable returning the delay length as a float
3440
    - Tuple of (start, factor, limit)
3441
    - L{RETRY_REMAINING_TIME} to sleep until the timeout expires (this is
3442
      useful when overriding L{wait_fn} to wait for an external event)
3443
    - A static delay as a number (int or float)
3444

  
3445
  @type fn: callable
3446
  @param fn: Function to be called
3447
  @param delay: Either a callable (returning the delay), a tuple of (start,
3448
                factor, limit) (see L{_RetryDelayCalculator}),
3449
                L{RETRY_REMAINING_TIME} or a number (int or float)
3450
  @type timeout: float
3451
  @param timeout: Total timeout
3452
  @type wait_fn: callable
3453
  @param wait_fn: Waiting function
3454
  @return: Return value of function
3455

  
3456
  """
3457
  assert callable(fn)
3458
  assert callable(wait_fn)
3459
  assert callable(_time_fn)
3460

  
3461
  if args is None:
3462
    args = []
3463

  
3464
  end_time = _time_fn() + timeout
3465

  
3466
  if callable(delay):
3467
    # External function to calculate delay
3468
    calc_delay = delay
3469

  
3470
  elif isinstance(delay, (tuple, list)):
3471
    # Increasing delay with optional upper boundary
3472
    (start, factor, limit) = delay
3473
    calc_delay = _RetryDelayCalculator(start, factor, limit)
3474

  
3475
  elif delay is RETRY_REMAINING_TIME:
3476
    # Always use the remaining time
3477
    calc_delay = None
3478

  
3479
  else:
3480
    # Static delay
3481
    calc_delay = lambda: delay
3482

  
3483
  assert calc_delay is None or callable(calc_delay)
3484

  
3485
  while True:
3486
    retry_args = []
3487
    try:
3488
      # pylint: disable-msg=W0142
3489
      return fn(*args)
3490
    except RetryAgain, err:
3491
      retry_args = err.args
3492
    except RetryTimeout:
3493
      raise errors.ProgrammerError("Nested retry loop detected that didn't"
3494
                                   " handle RetryTimeout")
3495

  
3496
    remaining_time = end_time - _time_fn()
3497

  
3498
    if remaining_time < 0.0:
3499
      # pylint: disable-msg=W0142
3500
      raise RetryTimeout(*retry_args)
3501

  
3502
    assert remaining_time >= 0.0
3503

  
3504
    if calc_delay is None:
3505
      wait_fn(remaining_time)
3506
    else:
3507
      current_delay = calc_delay()
3508
      if current_delay > 0.0:
3509
        wait_fn(current_delay)
3510

  
3511

  
3512 3357
def GetClosedTempfile(*args, **kwargs):
3513 3358
  """Creates a temporary file and returns its path.
3514 3359

  
b/lib/utils/retry.py
1
#
2
#
3

  
4
# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21
"""Utility functions for retrying function calls with a timeout.
22

  
23
"""
24

  
25

  
26
import time
27

  
28
from ganeti import errors
29

  
30

  
31
#: Special delay to specify whole remaining timeout
32
RETRY_REMAINING_TIME = object()
33

  
34

  
35
class RetryTimeout(Exception):
36
  """Retry loop timed out.
37

  
38
  Any arguments which was passed by the retried function to RetryAgain will be
39
  preserved in RetryTimeout, if it is raised. If such argument was an exception
40
  the RaiseInner helper method will reraise it.
41

  
42
  """
43
  def RaiseInner(self):
44
    if self.args and isinstance(self.args[0], Exception):
45
      raise self.args[0]
46
    else:
47
      raise RetryTimeout(*self.args)
48

  
49

  
50
class RetryAgain(Exception):
51
  """Retry again.
52

  
53
  Any arguments passed to RetryAgain will be preserved, if a timeout occurs, as
54
  arguments to RetryTimeout. If an exception is passed, the RaiseInner() method
55
  of the RetryTimeout() method can be used to reraise it.
56

  
57
  """
58

  
59

  
60
class _RetryDelayCalculator(object):
61
  """Calculator for increasing delays.
62

  
63
  """
64
  __slots__ = [
65
    "_factor",
66
    "_limit",
67
    "_next",
68
    "_start",
69
    ]
70

  
71
  def __init__(self, start, factor, limit):
72
    """Initializes this class.
73

  
74
    @type start: float
75
    @param start: Initial delay
76
    @type factor: float
77
    @param factor: Factor for delay increase
78
    @type limit: float or None
79
    @param limit: Upper limit for delay or None for no limit
80

  
81
    """
82
    assert start > 0.0
83
    assert factor >= 1.0
84
    assert limit is None or limit >= 0.0
85

  
86
    self._start = start
87
    self._factor = factor
88
    self._limit = limit
89

  
90
    self._next = start
91

  
92
  def __call__(self):
93
    """Returns current delay and calculates the next one.
94

  
95
    """
96
    current = self._next
97

  
98
    # Update for next run
99
    if self._limit is None or self._next < self._limit:
100
      self._next = min(self._limit, self._next * self._factor)
101

  
102
    return current
103

  
104

  
105
def Retry(fn, delay, timeout, args=None, wait_fn=time.sleep,
106
          _time_fn=time.time):
107
  """Call a function repeatedly until it succeeds.
108

  
109
  The function C{fn} is called repeatedly until it doesn't throw L{RetryAgain}
110
  anymore. Between calls a delay, specified by C{delay}, is inserted. After a
111
  total of C{timeout} seconds, this function throws L{RetryTimeout}.
112

  
113
  C{delay} can be one of the following:
114
    - callable returning the delay length as a float
115
    - Tuple of (start, factor, limit)
116
    - L{RETRY_REMAINING_TIME} to sleep until the timeout expires (this is
117
      useful when overriding L{wait_fn} to wait for an external event)
118
    - A static delay as a number (int or float)
119

  
120
  @type fn: callable
121
  @param fn: Function to be called
122
  @param delay: Either a callable (returning the delay), a tuple of (start,
123
                factor, limit) (see L{_RetryDelayCalculator}),
124
                L{RETRY_REMAINING_TIME} or a number (int or float)
125
  @type timeout: float
126
  @param timeout: Total timeout
127
  @type wait_fn: callable
128
  @param wait_fn: Waiting function
129
  @return: Return value of function
130

  
131
  """
132
  assert callable(fn)
133
  assert callable(wait_fn)
134
  assert callable(_time_fn)
135

  
136
  if args is None:
137
    args = []
138

  
139
  end_time = _time_fn() + timeout
140

  
141
  if callable(delay):
142
    # External function to calculate delay
143
    calc_delay = delay
144

  
145
  elif isinstance(delay, (tuple, list)):
146
    # Increasing delay with optional upper boundary
147
    (start, factor, limit) = delay
148
    calc_delay = _RetryDelayCalculator(start, factor, limit)
149

  
150
  elif delay is RETRY_REMAINING_TIME:
151
    # Always use the remaining time
152
    calc_delay = None
153

  
154
  else:
155
    # Static delay
156
    calc_delay = lambda: delay
157

  
158
  assert calc_delay is None or callable(calc_delay)
159

  
160
  while True:
161
    retry_args = []
162
    try:
163
      # pylint: disable-msg=W0142
164
      return fn(*args)
165
    except RetryAgain, err:
166
      retry_args = err.args
167
    except RetryTimeout:
168
      raise errors.ProgrammerError("Nested retry loop detected that didn't"
169
                                   " handle RetryTimeout")
170

  
171
    remaining_time = end_time - _time_fn()
172

  
173
    if remaining_time < 0.0:
174
      # pylint: disable-msg=W0142
175
      raise RetryTimeout(*retry_args)
176

  
177
    assert remaining_time >= 0.0
178

  
179
    if calc_delay is None:
180
      wait_fn(remaining_time)
181
    else:
182
      current_delay = calc_delay()
183
      if current_delay > 0.0:
184
        wait_fn(current_delay)
b/test/ganeti.utils.retry_unittest.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21

  
22
"""Script for testing ganeti.utils.retry"""
23

  
24
import unittest
25

  
26
from ganeti import constants
27
from ganeti import errors
28
from ganeti import utils
29

  
30
import testutils
31

  
32

  
33
class TestRetry(testutils.GanetiTestCase):
34
  def setUp(self):
35
    testutils.GanetiTestCase.setUp(self)
36
    self.retries = 0
37

  
38
  @staticmethod
39
  def _RaiseRetryAgain():
40
    raise utils.RetryAgain()
41

  
42
  @staticmethod
43
  def _RaiseRetryAgainWithArg(args):
44
    raise utils.RetryAgain(*args)
45

  
46
  def _WrongNestedLoop(self):
47
    return utils.Retry(self._RaiseRetryAgain, 0.01, 0.02)
48

  
49
  def _RetryAndSucceed(self, retries):
50
    if self.retries < retries:
51
      self.retries += 1
52
      raise utils.RetryAgain()
53
    else:
54
      return True
55

  
56
  def testRaiseTimeout(self):
57
    self.failUnlessRaises(utils.RetryTimeout, utils.Retry,
58
                          self._RaiseRetryAgain, 0.01, 0.02)
59
    self.failUnlessRaises(utils.RetryTimeout, utils.Retry,
60
                          self._RetryAndSucceed, 0.01, 0, args=[1])
61
    self.failUnlessEqual(self.retries, 1)
62

  
63
  def testComplete(self):
64
    self.failUnlessEqual(utils.Retry(lambda: True, 0, 1), True)
65
    self.failUnlessEqual(utils.Retry(self._RetryAndSucceed, 0, 1, args=[2]),
66
                         True)
67
    self.failUnlessEqual(self.retries, 2)
68

  
69
  def testNestedLoop(self):
70
    try:
71
      self.failUnlessRaises(errors.ProgrammerError, utils.Retry,
72
                            self._WrongNestedLoop, 0, 1)
73
    except utils.RetryTimeout:
74
      self.fail("Didn't detect inner loop's exception")
75

  
76
  def testTimeoutArgument(self):
77
    retry_arg="my_important_debugging_message"
78
    try:
79
      utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, args=[[retry_arg]])
80
    except utils.RetryTimeout, err:
81
      self.failUnlessEqual(err.args, (retry_arg, ))
82
    else:
83
      self.fail("Expected timeout didn't happen")
84

  
85
  def testRaiseInnerWithExc(self):
86
    retry_arg="my_important_debugging_message"
87
    try:
88
      try:
89
        utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02,
90
                    args=[[errors.GenericError(retry_arg, retry_arg)]])
91
      except utils.RetryTimeout, err:
92
        err.RaiseInner()
93
      else:
94
        self.fail("Expected timeout didn't happen")
95
    except errors.GenericError, err:
96
      self.failUnlessEqual(err.args, (retry_arg, retry_arg))
97
    else:
98
      self.fail("Expected GenericError didn't happen")
99

  
100
  def testRaiseInnerWithMsg(self):
101
    retry_arg="my_important_debugging_message"
102
    try:
103
      try:
104
        utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02,
105
                    args=[[retry_arg, retry_arg]])
106
      except utils.RetryTimeout, err:
107
        err.RaiseInner()
108
      else:
109
        self.fail("Expected timeout didn't happen")
110
    except utils.RetryTimeout, err:
111
      self.failUnlessEqual(err.args, (retry_arg, retry_arg))
112
    else:
113
      self.fail("Expected RetryTimeout didn't happen")
114

  
115

  
116
if __name__ == "__main__":
117
  testutils.GanetiTestProgram()
b/test/ganeti.utils_unittest.py
1995 1995
    self.assert_(os.path.isdir(path))
1996 1996

  
1997 1997

  
1998
class TestRetry(testutils.GanetiTestCase):
1999
  def setUp(self):
2000
    testutils.GanetiTestCase.setUp(self)
2001
    self.retries = 0
2002

  
2003
  @staticmethod
2004
  def _RaiseRetryAgain():
2005
    raise utils.RetryAgain()
2006

  
2007
  @staticmethod
2008
  def _RaiseRetryAgainWithArg(args):
2009
    raise utils.RetryAgain(*args)
2010

  
2011
  def _WrongNestedLoop(self):
2012
    return utils.Retry(self._RaiseRetryAgain, 0.01, 0.02)
2013

  
2014
  def _RetryAndSucceed(self, retries):
2015
    if self.retries < retries:
2016
      self.retries += 1
2017
      raise utils.RetryAgain()
2018
    else:
2019
      return True
2020

  
2021
  def testRaiseTimeout(self):
2022
    self.failUnlessRaises(utils.RetryTimeout, utils.Retry,
2023
                          self._RaiseRetryAgain, 0.01, 0.02)
2024
    self.failUnlessRaises(utils.RetryTimeout, utils.Retry,
2025
                          self._RetryAndSucceed, 0.01, 0, args=[1])
2026
    self.failUnlessEqual(self.retries, 1)
2027

  
2028
  def testComplete(self):
2029
    self.failUnlessEqual(utils.Retry(lambda: True, 0, 1), True)
2030
    self.failUnlessEqual(utils.Retry(self._RetryAndSucceed, 0, 1, args=[2]),
2031
                         True)
2032
    self.failUnlessEqual(self.retries, 2)
2033

  
2034
  def testNestedLoop(self):
2035
    try:
2036
      self.failUnlessRaises(errors.ProgrammerError, utils.Retry,
2037
                            self._WrongNestedLoop, 0, 1)
2038
    except utils.RetryTimeout:
2039
      self.fail("Didn't detect inner loop's exception")
2040

  
2041
  def testTimeoutArgument(self):
2042
    retry_arg="my_important_debugging_message"
2043
    try:
2044
      utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02, args=[[retry_arg]])
2045
    except utils.RetryTimeout, err:
2046
      self.failUnlessEqual(err.args, (retry_arg, ))
2047
    else:
2048
      self.fail("Expected timeout didn't happen")
2049

  
2050
  def testRaiseInnerWithExc(self):
2051
    retry_arg="my_important_debugging_message"
2052
    try:
2053
      try:
2054
        utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02,
2055
                    args=[[errors.GenericError(retry_arg, retry_arg)]])
2056
      except utils.RetryTimeout, err:
2057
        err.RaiseInner()
2058
      else:
2059
        self.fail("Expected timeout didn't happen")
2060
    except errors.GenericError, err:
2061
      self.failUnlessEqual(err.args, (retry_arg, retry_arg))
2062
    else:
2063
      self.fail("Expected GenericError didn't happen")
2064

  
2065
  def testRaiseInnerWithMsg(self):
2066
    retry_arg="my_important_debugging_message"
2067
    try:
2068
      try:
2069
        utils.Retry(self._RaiseRetryAgainWithArg, 0.01, 0.02,
2070
                    args=[[retry_arg, retry_arg]])
2071
      except utils.RetryTimeout, err:
2072
        err.RaiseInner()
2073
      else:
2074
        self.fail("Expected timeout didn't happen")
2075
    except utils.RetryTimeout, err:
2076
      self.failUnlessEqual(err.args, (retry_arg, retry_arg))
2077
    else:
2078
      self.fail("Expected RetryTimeout didn't happen")
2079

  
2080

  
2081 1998
class TestLineSplitter(unittest.TestCase):
2082 1999
  def test(self):
2083 2000
    lines = []

Also available in: Unified diff