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 | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 5 | import logging, random, signal, sys, time |
| 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 | 6d31f80 | 2013-01-11 14:46:12 -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_result=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_result: default return value for the function call. |
| 35 | |
| 36 | @return 1: is_timeout 2: result of the function call. If |
| 37 | is_timeout is True, the call is timed out. If the |
| 38 | value is False, the call is finished on time. |
| 39 | """ |
| 40 | old_handler = signal.signal(signal.SIGALRM, handler) |
| 41 | |
| 42 | timeout_sec_n = int(timeout_sec) |
| 43 | # In case the timeout is rounded to 0, force to set it to default value. |
| 44 | if timeout_sec_n == 0: |
| 45 | timeout_sec_n = 60 |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 46 | try: |
Dan Shi | c0eae51 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 47 | old_alarm_sec = signal.alarm(timeout_sec_n) |
| 48 | if old_alarm_sec > 0: |
| 49 | old_timeout_time = time.time() + old_alarm_sec |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 50 | default_result = func(*args, **kwargs) |
| 51 | return False, default_result |
| 52 | except TimeoutException: |
| 53 | return True, default_result |
| 54 | finally: |
| 55 | # Cancel the timer if the function returned before timeout or |
| 56 | # exception being thrown. |
| 57 | signal.alarm(0) |
| 58 | # Restore previous Signal handler and alarm |
| 59 | if old_handler: |
| 60 | signal.signal(signal.SIGALRM, old_handler) |
| 61 | if old_alarm_sec > 0: |
| 62 | old_alarm_sec = int(old_timeout_time - time.time()) |
| 63 | if old_alarm_sec <= 0: |
| 64 | old_alarm_sec = 1; |
| 65 | signal.alarm(old_alarm_sec) |
| 66 | |
| 67 | |
| 68 | def retry(ExceptionToCheck, timeout_min=1.0, delay_sec=3): |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 69 | """Retry calling the decorated function using a delay with jitter. |
| 70 | |
| 71 | Will raise RPC ValidationError exceptions from the decorated |
| 72 | function without retrying; a malformed RPC isn't going to |
| 73 | magically become good. |
| 74 | |
| 75 | original from: |
| 76 | http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ |
| 77 | |
| 78 | @param ExceptionToCheck: the exception to check. May be a tuple of |
| 79 | exceptions to check. |
| 80 | @param timeout_min: timeout in minutes until giving up. |
| 81 | @param delay_sec: pre-jittered delay between retries in seconds. Actual |
| 82 | delays will be centered around this value, ranging up to |
| 83 | 50% off this midpoint. |
| 84 | """ |
| 85 | def deco_retry(func): |
| 86 | random.seed() |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 87 | |
| 88 | |
| 89 | def delay(): |
| 90 | """ |
| 91 | 'Jitter' the delay, up to 50% in either direction. |
| 92 | """ |
| 93 | random_delay = random.uniform(.5 * delay_sec, 1.5 * delay_sec) |
| 94 | logging.warning('Retrying in %f seconds...', random_delay) |
| 95 | time.sleep(random_delay) |
| 96 | |
| 97 | |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 98 | def func_retry(*args, **kwargs): |
| 99 | deadline = time.time() + timeout_min * 60 # convert to seconds. |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 100 | # Used to cache exception to be raised later. |
| 101 | exc_info = None |
| 102 | delayed_enabled = False |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 103 | while time.time() < deadline: |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 104 | if delayed_enabled: |
| 105 | delay() |
| 106 | else: |
| 107 | delayed_enabled = True |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 108 | try: |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 109 | # Clear the cache |
| 110 | exc_info = None |
| 111 | is_timeout, result = timeout(func, args, kwargs, |
| 112 | timeout_min*60) |
| 113 | if not is_timeout: |
| 114 | return result |
| 115 | except (error.CrosDynamicSuiteException, |
| 116 | proxy.ValidationError): |
| 117 | raise |
| 118 | except ExceptionToCheck as e: |
| 119 | logging.warning('%s(%s)', e.__class__, e) |
| 120 | # Cache the exception to be raised later. |
| 121 | exc_info = sys.exc_info() |
| 122 | # The call must have timed out or raised ExceptionToCheck. |
| 123 | if not exc_info: |
| 124 | raise TimeoutException('Call is timed out.') |
| 125 | # Raise the cached exception with original backtrace. |
| 126 | raise exc_info[0], exc_info[1], exc_info[2] |
| 127 | |
| 128 | |
Chris Masone | 6f10908 | 2012-07-18 14:21:38 -0700 | [diff] [blame] | 129 | return func_retry # true decorator |
Dan Shi | 6d31f80 | 2013-01-11 14:46:12 -0800 | [diff] [blame] | 130 | return deco_retry |