| #!/usr/bin/env python |
| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| |
| """Generic retry wrapper for Git operations. |
| |
| This is largely DEPRECATED in favor of the Infra Git wrapper: |
| https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git |
| """ |
| |
| import logging |
| import optparse |
| import os |
| import subprocess |
| import sys |
| import threading |
| import time |
| |
| from git_common import GIT_EXE, GIT_TRANSIENT_ERRORS_RE |
| |
| |
| class TeeThread(threading.Thread): |
| |
| def __init__(self, fd, out_fd, name): |
| super(TeeThread, self).__init__(name='git-retry.tee.%s' % (name,)) |
| self.data = None |
| self.fd = fd |
| self.out_fd = out_fd |
| |
| def run(self): |
| chunks = [] |
| for line in self.fd: |
| chunks.append(line) |
| self.out_fd.write(line) |
| self.data = ''.join(chunks) |
| |
| |
| class GitRetry(object): |
| |
| logger = logging.getLogger('git-retry') |
| DEFAULT_DELAY_SECS = 3.0 |
| DEFAULT_RETRY_COUNT = 5 |
| |
| def __init__(self, retry_count=None, delay=None, delay_factor=None): |
| self.retry_count = retry_count or self.DEFAULT_RETRY_COUNT |
| self.delay = max(delay, 0) if delay else 0 |
| self.delay_factor = max(delay_factor, 0) if delay_factor else 0 |
| |
| def shouldRetry(self, stderr): |
| m = GIT_TRANSIENT_ERRORS_RE.search(stderr) |
| if not m: |
| return False |
| self.logger.info("Encountered known transient error: [%s]", |
| stderr[m.start(): m.end()]) |
| return True |
| |
| @staticmethod |
| def execute(*args): |
| args = (GIT_EXE,) + args |
| proc = subprocess.Popen( |
| args, |
| stderr=subprocess.PIPE, |
| ) |
| stderr_tee = TeeThread(proc.stderr, sys.stderr, 'stderr') |
| |
| # Start our process. Collect/tee 'stdout' and 'stderr'. |
| stderr_tee.start() |
| try: |
| proc.wait() |
| except KeyboardInterrupt: |
| proc.kill() |
| raise |
| finally: |
| stderr_tee.join() |
| return proc.returncode, None, stderr_tee.data |
| |
| def computeDelay(self, iteration): |
| """Returns: the delay (in seconds) for a given iteration |
| |
| The first iteration has a delay of '0'. |
| |
| Args: |
| iteration: (int) The iteration index (starting with zero as the first |
| iteration) |
| """ |
| if (not self.delay) or (iteration == 0): |
| return 0 |
| if self.delay_factor == 0: |
| # Linear delay |
| return iteration * self.delay |
| # Exponential delay |
| return (self.delay_factor ** (iteration - 1)) * self.delay |
| |
| def __call__(self, *args): |
| returncode = 0 |
| for i in xrange(self.retry_count): |
| # If the previous run failed and a delay is configured, delay before the |
| # next run. |
| delay = self.computeDelay(i) |
| if delay > 0: |
| self.logger.info("Delaying for [%s second(s)] until next retry", delay) |
| time.sleep(delay) |
| |
| self.logger.debug("Executing subprocess (%d/%d) with arguments: %s", |
| (i+1), self.retry_count, args) |
| returncode, _, stderr = self.execute(*args) |
| |
| self.logger.debug("Process terminated with return code: %d", returncode) |
| if returncode == 0: |
| break |
| |
| if not self.shouldRetry(stderr): |
| self.logger.error("Process failure was not known to be transient; " |
| "terminating with return code %d", returncode) |
| break |
| return returncode |
| |
| |
| def main(args): |
| # If we're using the Infra Git wrapper, do nothing here. |
| # https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git |
| if 'INFRA_GIT_WRAPPER' in os.environ: |
| # Remove Git's execution path from PATH so that our call-through re-invokes |
| # the Git wrapper. |
| # See crbug.com/721450 |
| env = os.environ.copy() |
| git_exec = subprocess.check_output([GIT_EXE, '--exec-path']).strip() |
| env['PATH'] = os.pathsep.join([ |
| elem for elem in env.get('PATH', '').split(os.pathsep) |
| if elem != git_exec]) |
| return subprocess.call([GIT_EXE] + args, env=env) |
| |
| parser = optparse.OptionParser() |
| parser.disable_interspersed_args() |
| parser.add_option('-v', '--verbose', |
| action='count', default=0, |
| help="Increase verbosity; can be specified multiple times") |
| parser.add_option('-c', '--retry-count', metavar='COUNT', |
| type=int, default=GitRetry.DEFAULT_RETRY_COUNT, |
| help="Number of times to retry (default=%default)") |
| parser.add_option('-d', '--delay', metavar='SECONDS', |
| type=float, default=GitRetry.DEFAULT_DELAY_SECS, |
| help="Specifies the amount of time (in seconds) to wait " |
| "between successive retries (default=%default). This " |
| "can be zero.") |
| parser.add_option('-D', '--delay-factor', metavar='FACTOR', |
| type=int, default=2, |
| help="The exponential factor to apply to delays in between " |
| "successive failures (default=%default). If this is " |
| "zero, delays will increase linearly. Set this to " |
| "one to have a constant (non-increasing) delay.") |
| |
| opts, args = parser.parse_args(args) |
| |
| # Configure logging verbosity |
| if opts.verbose == 0: |
| logging.getLogger().setLevel(logging.WARNING) |
| elif opts.verbose == 1: |
| logging.getLogger().setLevel(logging.INFO) |
| else: |
| logging.getLogger().setLevel(logging.DEBUG) |
| |
| # Execute retries |
| retry = GitRetry( |
| retry_count=opts.retry_count, |
| delay=opts.delay, |
| delay_factor=opts.delay_factor, |
| ) |
| return retry(*args) |
| |
| |
| if __name__ == '__main__': |
| logging.basicConfig() |
| logging.getLogger().setLevel(logging.WARNING) |
| try: |
| sys.exit(main(sys.argv[2:])) |
| except KeyboardInterrupt: |
| sys.stderr.write('interrupted\n') |
| sys.exit(1) |