[email protected] | de219ec | 2014-07-28 17:39:08 | [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 | |
Dan Jacques | 2f8b0c1 | 2017-04-05 19:57:21 | [diff] [blame] | 6 | |
| 7 | """Generic retry wrapper for Git operations. |
| 8 | |
| 9 | This is largely DEPRECATED in favor of the Infra Git wrapper: |
| 10 | https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git |
| 11 | """ |
| 12 | |
[email protected] | de219ec | 2014-07-28 17:39:08 | [diff] [blame] | 13 | import logging |
| 14 | import optparse |
Dan Jacques | 2f8b0c1 | 2017-04-05 19:57:21 | [diff] [blame] | 15 | import os |
[email protected] | de219ec | 2014-07-28 17:39:08 | [diff] [blame] | 16 | import subprocess |
| 17 | import sys |
| 18 | import threading |
| 19 | import time |
| 20 | |
| 21 | from git_common import GIT_EXE, GIT_TRANSIENT_ERRORS_RE |
| 22 | |
| 23 | |
| 24 | class TeeThread(threading.Thread): |
| 25 | |
| 26 | def __init__(self, fd, out_fd, name): |
| 27 | super(TeeThread, self).__init__(name='git-retry.tee.%s' % (name,)) |
| 28 | self.data = None |
| 29 | self.fd = fd |
| 30 | self.out_fd = out_fd |
| 31 | |
| 32 | def run(self): |
| 33 | chunks = [] |
| 34 | for line in self.fd: |
| 35 | chunks.append(line) |
| 36 | self.out_fd.write(line) |
| 37 | self.data = ''.join(chunks) |
| 38 | |
| 39 | |
| 40 | class GitRetry(object): |
| 41 | |
| 42 | logger = logging.getLogger('git-retry') |
| 43 | DEFAULT_DELAY_SECS = 3.0 |
| 44 | DEFAULT_RETRY_COUNT = 5 |
| 45 | |
| 46 | def __init__(self, retry_count=None, delay=None, delay_factor=None): |
| 47 | self.retry_count = retry_count or self.DEFAULT_RETRY_COUNT |
| 48 | self.delay = max(delay, 0) if delay else 0 |
| 49 | self.delay_factor = max(delay_factor, 0) if delay_factor else 0 |
| 50 | |
| 51 | def shouldRetry(self, stderr): |
| 52 | m = GIT_TRANSIENT_ERRORS_RE.search(stderr) |
| 53 | if not m: |
| 54 | return False |
| 55 | self.logger.info("Encountered known transient error: [%s]", |
| 56 | stderr[m.start(): m.end()]) |
| 57 | return True |
| 58 | |
| 59 | @staticmethod |
| 60 | def execute(*args): |
| 61 | args = (GIT_EXE,) + args |
| 62 | proc = subprocess.Popen( |
| 63 | args, |
| 64 | stderr=subprocess.PIPE, |
| 65 | ) |
| 66 | stderr_tee = TeeThread(proc.stderr, sys.stderr, 'stderr') |
| 67 | |
| 68 | # Start our process. Collect/tee 'stdout' and 'stderr'. |
| 69 | stderr_tee.start() |
| 70 | try: |
| 71 | proc.wait() |
| 72 | except KeyboardInterrupt: |
| 73 | proc.kill() |
| 74 | raise |
| 75 | finally: |
| 76 | stderr_tee.join() |
| 77 | return proc.returncode, None, stderr_tee.data |
| 78 | |
| 79 | def computeDelay(self, iteration): |
| 80 | """Returns: the delay (in seconds) for a given iteration |
| 81 | |
| 82 | The first iteration has a delay of '0'. |
| 83 | |
| 84 | Args: |
| 85 | iteration: (int) The iteration index (starting with zero as the first |
| 86 | iteration) |
| 87 | """ |
| 88 | if (not self.delay) or (iteration == 0): |
| 89 | return 0 |
| 90 | if self.delay_factor == 0: |
| 91 | # Linear delay |
| 92 | return iteration * self.delay |
| 93 | # Exponential delay |
| 94 | return (self.delay_factor ** (iteration - 1)) * self.delay |
| 95 | |
| 96 | def __call__(self, *args): |
| 97 | returncode = 0 |
| 98 | for i in xrange(self.retry_count): |
| 99 | # If the previous run failed and a delay is configured, delay before the |
| 100 | # next run. |
| 101 | delay = self.computeDelay(i) |
| 102 | if delay > 0: |
| 103 | self.logger.info("Delaying for [%s second(s)] until next retry", delay) |
| 104 | time.sleep(delay) |
| 105 | |
| 106 | self.logger.debug("Executing subprocess (%d/%d) with arguments: %s", |
| 107 | (i+1), self.retry_count, args) |
| 108 | returncode, _, stderr = self.execute(*args) |
| 109 | |
| 110 | self.logger.debug("Process terminated with return code: %d", returncode) |
| 111 | if returncode == 0: |
| 112 | break |
| 113 | |
| 114 | if not self.shouldRetry(stderr): |
| 115 | self.logger.error("Process failure was not known to be transient; " |
| 116 | "terminating with return code %d", returncode) |
| 117 | break |
| 118 | return returncode |
| 119 | |
| 120 | |
| 121 | def main(args): |
Dan Jacques | 2f8b0c1 | 2017-04-05 19:57:21 | [diff] [blame] | 122 | # If we're using the Infra Git wrapper, do nothing here. |
| 123 | # https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git |
| 124 | if 'INFRA_GIT_WRAPPER' in os.environ: |
Dan Jacques | e2af38e | 2017-05-16 00:51:20 | [diff] [blame] | 125 | # Remove Git's execution path from PATH so that our call-through re-invokes |
| 126 | # the Git wrapper. |
| 127 | # See crbug.com/721450 |
| 128 | env = os.environ.copy() |
| 129 | git_exec = subprocess.check_output([GIT_EXE, '--exec-path']).strip() |
| 130 | env['PATH'] = os.pathsep.join([ |
| 131 | elem for elem in env.get('PATH', '').split(os.pathsep) |
| 132 | if elem != git_exec]) |
| 133 | return subprocess.call([GIT_EXE] + args, env=env) |
Dan Jacques | 2f8b0c1 | 2017-04-05 19:57:21 | [diff] [blame] | 134 | |
[email protected] | de219ec | 2014-07-28 17:39:08 | [diff] [blame] | 135 | parser = optparse.OptionParser() |
| 136 | parser.disable_interspersed_args() |
| 137 | parser.add_option('-v', '--verbose', |
| 138 | action='count', default=0, |
| 139 | help="Increase verbosity; can be specified multiple times") |
| 140 | parser.add_option('-c', '--retry-count', metavar='COUNT', |
| 141 | type=int, default=GitRetry.DEFAULT_RETRY_COUNT, |
| 142 | help="Number of times to retry (default=%default)") |
| 143 | parser.add_option('-d', '--delay', metavar='SECONDS', |
| 144 | type=float, default=GitRetry.DEFAULT_DELAY_SECS, |
| 145 | help="Specifies the amount of time (in seconds) to wait " |
| 146 | "between successive retries (default=%default). This " |
| 147 | "can be zero.") |
| 148 | parser.add_option('-D', '--delay-factor', metavar='FACTOR', |
| 149 | type=int, default=2, |
| 150 | help="The exponential factor to apply to delays in between " |
| 151 | "successive failures (default=%default). If this is " |
| 152 | "zero, delays will increase linearly. Set this to " |
| 153 | "one to have a constant (non-increasing) delay.") |
| 154 | |
| 155 | opts, args = parser.parse_args(args) |
| 156 | |
| 157 | # Configure logging verbosity |
| 158 | if opts.verbose == 0: |
| 159 | logging.getLogger().setLevel(logging.WARNING) |
| 160 | elif opts.verbose == 1: |
| 161 | logging.getLogger().setLevel(logging.INFO) |
| 162 | else: |
| 163 | logging.getLogger().setLevel(logging.DEBUG) |
| 164 | |
| 165 | # Execute retries |
| 166 | retry = GitRetry( |
| 167 | retry_count=opts.retry_count, |
| 168 | delay=opts.delay, |
| 169 | delay_factor=opts.delay_factor, |
| 170 | ) |
| 171 | return retry(*args) |
| 172 | |
| 173 | |
| 174 | if __name__ == '__main__': |
| 175 | logging.basicConfig() |
| 176 | logging.getLogger().setLevel(logging.WARNING) |
[email protected] | 013731e | 2015-02-26 18:28:43 | [diff] [blame] | 177 | try: |
| 178 | sys.exit(main(sys.argv[2:])) |
| 179 | except KeyboardInterrupt: |
| 180 | sys.stderr.write('interrupted\n') |
| 181 | sys.exit(1) |