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.
5 """Basic infrastructure for implementing retries."""
7 from __future__ import print_function
13 from chromite.lib import cros_build_lib
16 def GenericRetry(handler, max_retry, functor, *args, **kwargs):
17 """Generic retry loop w/ optional break out depending on exceptions.
19 To retry based on the return value of |functor| see the timeout_util module.
22 handler: A functor invoked w/ the exception instance that
23 functor(*args, **kwargs) threw. If it returns True, then a
24 retry is attempted. If False, the exception is re-raised.
25 max_retry: A positive integer representing how many times to retry
26 the command before giving up. Worst case, the command is invoked
27 (max_retry + 1) times before failing.
28 functor: A callable to pass args and kwargs to.
29 args: Positional args passed to functor.
30 kwargs: Optional args passed to functor.
31 sleep: Optional keyword. Multiplier for how long to sleep between
32 retries; will delay (1*sleep) the first time, then (2*sleep),
33 continuing via attempt * sleep.
36 Whatever functor(*args, **kwargs) returns.
39 Exception: Whatever exceptions functor(*args, **kwargs) throws and
40 isn't suppressed is raised. Note that the first exception encountered
44 sleep = kwargs.pop('sleep', 0)
46 raise ValueError('max_retry needs to be zero or more: %s' % max_retry)
49 for attempt in xrange(max_retry + 1):
51 time.sleep(sleep * attempt)
53 return functor(*args, **kwargs)
54 except Exception as e:
55 # Note we're not snagging BaseException, so MemoryError/KeyboardInterrupt
56 # and friends don't enter this except block.
59 # We intentionally ignore any failures in later attempts since we'll
60 # throw the original failure if all retries fail.
62 exc_info = sys.exc_info()
64 raise exc_info[0], exc_info[1], exc_info[2]
67 def RetryException(exc_retry, max_retry, functor, *args, **kwargs):
68 """Convience wrapper for RetryInvocation based on exceptions.
71 exc_retry: A class (or tuple of classes). If the raised exception
72 is the given class(es), a retry will be attempted. Otherwise,
73 the exception is raised.
74 max_retry: See GenericRetry.
75 functor: See GenericRetry.
76 *args: See GenericRetry.
77 **kwargs: See GenericRetry.
79 if not isinstance(exc_retry, (tuple, type)):
80 raise TypeError('exc_retry should be an exception (or tuple), not %r' %
82 #pylint: disable=E0102
83 def exc_retry(exc, values=exc_retry):
84 return isinstance(exc, values)
85 return GenericRetry(exc_retry, max_retry, functor, *args, **kwargs)
88 def RetryCommand(functor, max_retry, *args, **kwargs):
89 """Wrapper for RunCommand that will retry a command
92 functor: RunCommand function to run; retries will only occur on
93 RunCommandError exceptions being thrown.
94 max_retry: A positive integer representing how many times to retry
95 the command before giving up. Worst case, the command is invoked
96 (max_retry + 1) times before failing.
97 sleep: Optional keyword. Multiplier for how long to sleep between
98 retries; will delay (1*sleep) the first time, then (2*sleep),
99 continuing via attempt * sleep.
100 retry_on: If provided, we will retry on any exit codes in the given list.
101 Note: A process will exit with a negative exit code if it is killed by a
102 signal. By default, we retry on all non-negative exit codes.
103 args: Positional args passed to RunCommand; see RunCommand for specifics.
104 kwargs: Optional args passed to RunCommand; see RunCommand for specifics.
107 A CommandResult object.
110 Exception: Raises RunCommandError on error with optional error_message.
112 values = kwargs.pop('retry_on', None)
113 def ShouldRetry(exc):
114 """Return whether we should retry on a given exception."""
115 if not ShouldRetryCommandCommon(exc):
117 if values is None and exc.result.returncode < 0:
118 logging.info('Child process received signal %d; not retrying.',
119 -exc.result.returncode)
121 return values is None or exc.result.returncode in values
122 return GenericRetry(ShouldRetry, max_retry, functor, *args, **kwargs)
125 def ShouldRetryCommandCommon(exc):
126 """Returns whether any RunCommand should retry on a given exception."""
127 if not isinstance(exc, cros_build_lib.RunCommandError):
129 if exc.result.returncode is None:
130 logging.info('Child process failed to launch; not retrying.')
135 def RunCommandWithRetries(max_retry, *args, **kwargs):
136 """Wrapper for RunCommand that will retry a command
139 max_retry: See RetryCommand and RunCommand.
140 *args: See RetryCommand and RunCommand.
141 **kwargs: See RetryCommand and RunCommand.
144 A CommandResult object.
147 Exception: Raises RunCommandError on error with optional error_message.
149 return RetryCommand(cros_build_lib.RunCommand, max_retry, *args, **kwargs)
152 def RunCurl(args, **kwargs):
153 """Runs curl and wraps around all necessary hacks."""
157 # These values were discerned via scraping the curl manpage; they're all
158 # retry related (dns failed, timeout occurred, etc, see the manpage for
159 # exact specifics of each).
160 # Note we allow 22 to deal w/ 500's- they're thrown by google storage
162 # Note we allow 35 to deal w/ Unknown SSL Protocol error, thrown by
163 # google storage occasionally.
164 # Finally, we do not use curl's --retry option since it generally doesn't
165 # actually retry anything; code 18 for example, it will not retry on.
166 retriable_exits = frozenset([5, 6, 7, 15, 18, 22, 26, 28, 35, 52, 56])
168 return RunCommandWithRetries(5, cmd, sleep=3, retry_on=retriable_exits,
170 except cros_build_lib.RunCommandError as e:
171 code = e.result.returncode
172 if code in (51, 58, 60):
173 # These are the return codes of failing certs as per 'man curl'.
174 msg = 'Download failed with certificate error? Try "sudo c_rehash".'
175 cros_build_lib.Die(msg)
178 return RunCommandWithRetries(5, cmd, sleep=60, retry_on=retriable_exits,
180 except cros_build_lib.RunCommandError as e:
181 cros_build_lib.Die("Curl failed w/ exit code %i", code)