| Index: git_retry.py
|
| diff --git a/git_retry.py b/git_retry.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..b40e6d2ef8f49034342c74a312419189000c267e
|
| --- /dev/null
|
| +++ b/git_retry.py
|
| @@ -0,0 +1,156 @@
|
| +#!/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.
|
| +
|
| +import logging
|
| +import optparse
|
| +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):
|
| + 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)
|
| + sys.exit(main(sys.argv[2:]))
|
|
|