Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 5 | import logging, random, time, signal, sys |
| 6 | |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 7 | from autotest_lib.client.common_lib import error |
| 8 | from autotest_lib.frontend.afe.json_rpc import proxy |
| 9 | |
| 10 | |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 11 | class TimeoutException(Exception): |
| 12 | """ |
| 13 | Exception to be raised for when alarm is triggered. |
| 14 | """ |
| 15 | pass |
| 16 | |
| 17 | |
| 18 | def handler(signum, frame): |
| 19 | """ |
| 20 | Register a handler for the timeout. |
| 21 | """ |
| 22 | raise TimeoutException('Call is timed out.') |
| 23 | |
| 24 | |
| 25 | def timeout(func, args=(), kwargs={}, timeout_sec=60.0, default=None): |
| 26 | """ |
| 27 | This function run the given function using the args, kwargs and |
| 28 | return the given default value if the timeout_sec is exceeded. |
| 29 | |
| 30 | @param func: function to be called. |
| 31 | @param args: arguments for function to be called. |
| 32 | @param kwargs: keyword arguments for function to be called. |
| 33 | @param timeout_sec: timeout setting for call to exit, in seconds. |
| 34 | @param default: default return value for the function call. |
| 35 | @return 1: is_timeout 2: result of the function call. If |
| 36 | is_timeout is True, the call is timed out. If the |
| 37 | value is False, the call is finished on time. |
| 38 | """ |
| 39 | old_handler = signal.signal(signal.SIGALRM, handler) |
| 40 | |
| 41 | timeout_sec_n = int(timeout_sec) |
| 42 | # In case the timeout is rounded to 0, force to set it to default value. |
| 43 | if timeout_sec_n == 0: |
| 44 | timeout_sec_n = 60 |
| 45 | old_alarm_sec = signal.alarm(timeout_sec_n) |
| 46 | if old_alarm_sec > 0: |
| 47 | old_timeout_time = time.time() + old_alarm_sec |
| 48 | try: |
| 49 | result = func(*args, **kwargs) |
| 50 | # Cancel the timer if the function returned before timeout |
| 51 | signal.alarm(0) |
| 52 | return False, result |
| 53 | except TimeoutException: |
| 54 | return True, default |
| 55 | finally: |
| 56 | # Restore previous Signal handler and alarm |
| 57 | if old_handler is not None: |
| 58 | signal.signal(signal.SIGALRM, old_handler) |
| 59 | if old_alarm_sec > 0: |
| 60 | old_alarm_sec = int(old_timeout_time - time.time()) |
| 61 | if old_alarm_sec <= 0: |
| 62 | old_alarm_sec = 1; |
| 63 | signal.alarm(old_alarm_sec) |
| 64 | |
| 65 | |
| 66 | def retry(ExceptionToCheck, timeout_min=1.0, delay_sec=3): |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 67 | """Retry calling the decorated function using a delay with jitter. |
| 68 | |
| 69 | Will raise RPC ValidationError exceptions from the decorated |
| 70 | function without retrying; a malformed RPC isn't going to |
| 71 | magically become good. |
| 72 | |
| 73 | original from: |
| 74 | http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ |
| 75 | |
| 76 | @param ExceptionToCheck: the exception to check. May be a tuple of |
| 77 | exceptions to check. |
| 78 | @param timeout_min: timeout in minutes until giving up. |
| 79 | @param delay_sec: pre-jittered delay between retries in seconds. Actual |
| 80 | delays will be centered around this value, ranging up to |
| 81 | 50% off this midpoint. |
| 82 | """ |
| 83 | def deco_retry(func): |
| 84 | random.seed() |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 85 | |
| 86 | |
| 87 | def delay(): |
| 88 | """ |
| 89 | 'Jitter' the delay, up to 50% in either direction. |
| 90 | """ |
| 91 | random_delay = random.uniform(.5 * delay_sec, 1.5 * delay_sec) |
| 92 | logging.warning("Retrying in %f seconds...", random_delay) |
| 93 | time.sleep(random_delay) |
| 94 | |
| 95 | |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 96 | def func_retry(*args, **kwargs): |
| 97 | deadline = time.time() + timeout_min * 60 # convert to seconds. |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 98 | # Used to cache exception to be raised later. |
| 99 | exc_info = None |
| 100 | delayed_enabled = False |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 101 | while time.time() < deadline: |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 102 | if delayed_enabled: |
| 103 | delay() |
| 104 | else: |
| 105 | delayed_enabled = True |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 106 | try: |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 107 | # Clear the cache |
| 108 | exc_info = None |
| 109 | is_timeout, result = timeout(func, args, kwargs, |
| 110 | timeout_min*60) |
| 111 | if not is_timeout: |
| 112 | return result |
| 113 | except (error.CrosDynamicSuiteException, |
| 114 | proxy.ValidationError): |
| 115 | raise |
| 116 | except ExceptionToCheck as e: |
| 117 | logging.warning("%s(%s)", e.__class__, e) |
| 118 | # Cache the exception to be raised later. |
| 119 | exc_info = sys.exc_info() |
| 120 | # The call must have timed out or raised ExceptionToCheck. |
| 121 | if exc_info is None: |
| 122 | raise TimeoutException('Call is timed out.') |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 123 | else: |
Dan Shi | 7b33c1a | 2012-12-21 11:34:52 -0800 | [diff] [blame^] | 124 | # Raise the cached exception with original backtrace. |
| 125 | raise exc_info[0], exc_info[1], exc_info[2] |
| 126 | |
| 127 | |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 128 | return func_retry # true decorator |
| 129 | return deco_retry |