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:])) |