OLD | NEW |
(Empty) | |
| 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:])) |
OLD | NEW |