dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | import logging |
| 7 | import optparse |
| 8 | import subprocess |
| 9 | import sys |
| 10 | import threading |
| 11 | import time |
| 12 | |
| 13 | from git_common import GIT_EXE, GIT_TRANSIENT_ERRORS_RE |
| 14 | |
| 15 | |
| 16 | class TeeThread(threading.Thread): |
| 17 | |
| 18 | def __init__(self, fd, out_fd, name): |
| 19 | super(TeeThread, self).__init__(name='git-retry.tee.%s' % (name,)) |
| 20 | self.data = None |
| 21 | self.fd = fd |
| 22 | self.out_fd = out_fd |
| 23 | |
| 24 | def run(self): |
| 25 | chunks = [] |
| 26 | for line in self.fd: |
| 27 | chunks.append(line) |
| 28 | self.out_fd.write(line) |
| 29 | self.data = ''.join(chunks) |
| 30 | |
| 31 | |
| 32 | class GitRetry(object): |
| 33 | |
| 34 | logger = logging.getLogger('git-retry') |
| 35 | DEFAULT_DELAY_SECS = 3.0 |
| 36 | DEFAULT_RETRY_COUNT = 5 |
| 37 | |
| 38 | def __init__(self, retry_count=None, delay=None, delay_factor=None): |
| 39 | self.retry_count = retry_count or self.DEFAULT_RETRY_COUNT |
| 40 | self.delay = max(delay, 0) if delay else 0 |
| 41 | self.delay_factor = max(delay_factor, 0) if delay_factor else 0 |
| 42 | |
| 43 | def shouldRetry(self, stderr): |
| 44 | m = GIT_TRANSIENT_ERRORS_RE.search(stderr) |
| 45 | if not m: |
| 46 | return False |
| 47 | self.logger.info("Encountered known transient error: [%s]", |
| 48 | stderr[m.start(): m.end()]) |
| 49 | return True |
| 50 | |
| 51 | @staticmethod |
| 52 | def execute(*args): |
| 53 | args = (GIT_EXE,) + args |
| 54 | proc = subprocess.Popen( |
| 55 | args, |
| 56 | stderr=subprocess.PIPE, |
| 57 | ) |
| 58 | stderr_tee = TeeThread(proc.stderr, sys.stderr, 'stderr') |
| 59 | |
| 60 | # Start our process. Collect/tee 'stdout' and 'stderr'. |
| 61 | stderr_tee.start() |
| 62 | try: |
| 63 | proc.wait() |
| 64 | except KeyboardInterrupt: |
| 65 | proc.kill() |
| 66 | raise |
| 67 | finally: |
| 68 | stderr_tee.join() |
| 69 | return proc.returncode, None, stderr_tee.data |
| 70 | |
| 71 | def computeDelay(self, iteration): |
| 72 | """Returns: the delay (in seconds) for a given iteration |
| 73 | |
| 74 | The first iteration has a delay of '0'. |
| 75 | |
| 76 | Args: |
| 77 | iteration: (int) The iteration index (starting with zero as the first |
| 78 | iteration) |
| 79 | """ |
| 80 | if (not self.delay) or (iteration == 0): |
| 81 | return 0 |
| 82 | if self.delay_factor == 0: |
| 83 | # Linear delay |
| 84 | return iteration * self.delay |
| 85 | # Exponential delay |
| 86 | return (self.delay_factor ** (iteration - 1)) * self.delay |
| 87 | |
| 88 | def __call__(self, *args): |
| 89 | returncode = 0 |
| 90 | for i in xrange(self.retry_count): |
| 91 | # If the previous run failed and a delay is configured, delay before the |
| 92 | # next run. |
| 93 | delay = self.computeDelay(i) |
| 94 | if delay > 0: |
| 95 | self.logger.info("Delaying for [%s second(s)] until next retry", delay) |
| 96 | time.sleep(delay) |
| 97 | |
| 98 | self.logger.debug("Executing subprocess (%d/%d) with arguments: %s", |
| 99 | (i+1), self.retry_count, args) |
| 100 | returncode, _, stderr = self.execute(*args) |
| 101 | |
| 102 | self.logger.debug("Process terminated with return code: %d", returncode) |
| 103 | if returncode == 0: |
| 104 | break |
| 105 | |
| 106 | if not self.shouldRetry(stderr): |
| 107 | self.logger.error("Process failure was not known to be transient; " |
| 108 | "terminating with return code %d", returncode) |
| 109 | break |
| 110 | return returncode |
| 111 | |
| 112 | |
| 113 | def main(args): |
| 114 | parser = optparse.OptionParser() |
| 115 | parser.disable_interspersed_args() |
| 116 | parser.add_option('-v', '--verbose', |
| 117 | action='count', default=0, |
| 118 | help="Increase verbosity; can be specified multiple times") |
| 119 | parser.add_option('-c', '--retry-count', metavar='COUNT', |
| 120 | type=int, default=GitRetry.DEFAULT_RETRY_COUNT, |
| 121 | help="Number of times to retry (default=%default)") |
| 122 | parser.add_option('-d', '--delay', metavar='SECONDS', |
| 123 | type=float, default=GitRetry.DEFAULT_DELAY_SECS, |
| 124 | help="Specifies the amount of time (in seconds) to wait " |
| 125 | "between successive retries (default=%default). This " |
| 126 | "can be zero.") |
| 127 | parser.add_option('-D', '--delay-factor', metavar='FACTOR', |
| 128 | type=int, default=2, |
| 129 | help="The exponential factor to apply to delays in between " |
| 130 | "successive failures (default=%default). If this is " |
| 131 | "zero, delays will increase linearly. Set this to " |
| 132 | "one to have a constant (non-increasing) delay.") |
| 133 | |
| 134 | opts, args = parser.parse_args(args) |
| 135 | |
| 136 | # Configure logging verbosity |
| 137 | if opts.verbose == 0: |
| 138 | logging.getLogger().setLevel(logging.WARNING) |
| 139 | elif opts.verbose == 1: |
| 140 | logging.getLogger().setLevel(logging.INFO) |
| 141 | else: |
| 142 | logging.getLogger().setLevel(logging.DEBUG) |
| 143 | |
| 144 | # Execute retries |
| 145 | retry = GitRetry( |
| 146 | retry_count=opts.retry_count, |
| 147 | delay=opts.delay, |
| 148 | delay_factor=opts.delay_factor, |
| 149 | ) |
| 150 | return retry(*args) |
| 151 | |
| 152 | |
| 153 | if __name__ == '__main__': |
| 154 | logging.basicConfig() |
| 155 | logging.getLogger().setLevel(logging.WARNING) |
| 156 | sys.exit(main(sys.argv[2:])) |