git_retry.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. import logging
  6. import optparse
  7. import subprocess
  8. import sys
  9. import threading
  10. import time
  11. from git_common import GIT_EXE, GIT_TRANSIENT_ERRORS_RE
  12. class TeeThread(threading.Thread):
  13. def __init__(self, fd, out_fd, name):
  14. super(TeeThread, self).__init__(name='git-retry.tee.%s' % (name,))
  15. self.data = None
  16. self.fd = fd
  17. self.out_fd = out_fd
  18. def run(self):
  19. chunks = []
  20. for line in self.fd:
  21. chunks.append(line)
  22. self.out_fd.write(line)
  23. self.data = ''.join(chunks)
  24. class GitRetry(object):
  25. logger = logging.getLogger('git-retry')
  26. DEFAULT_DELAY_SECS = 3.0
  27. DEFAULT_RETRY_COUNT = 5
  28. def __init__(self, retry_count=None, delay=None, delay_factor=None):
  29. self.retry_count = retry_count or self.DEFAULT_RETRY_COUNT
  30. self.delay = max(delay, 0) if delay else 0
  31. self.delay_factor = max(delay_factor, 0) if delay_factor else 0
  32. def shouldRetry(self, stderr):
  33. m = GIT_TRANSIENT_ERRORS_RE.search(stderr)
  34. if not m:
  35. return False
  36. self.logger.info("Encountered known transient error: [%s]",
  37. stderr[m.start(): m.end()])
  38. return True
  39. @staticmethod
  40. def execute(*args):
  41. args = (GIT_EXE,) + args
  42. proc = subprocess.Popen(
  43. args,
  44. stderr=subprocess.PIPE,
  45. )
  46. stderr_tee = TeeThread(proc.stderr, sys.stderr, 'stderr')
  47. # Start our process. Collect/tee 'stdout' and 'stderr'.
  48. stderr_tee.start()
  49. try:
  50. proc.wait()
  51. except KeyboardInterrupt:
  52. proc.kill()
  53. raise
  54. finally:
  55. stderr_tee.join()
  56. return proc.returncode, None, stderr_tee.data
  57. def computeDelay(self, iteration):
  58. """Returns: the delay (in seconds) for a given iteration
  59. The first iteration has a delay of '0'.
  60. Args:
  61. iteration: (int) The iteration index (starting with zero as the first
  62. iteration)
  63. """
  64. if (not self.delay) or (iteration == 0):
  65. return 0
  66. if self.delay_factor == 0:
  67. # Linear delay
  68. return iteration * self.delay
  69. # Exponential delay
  70. return (self.delay_factor ** (iteration - 1)) * self.delay
  71. def __call__(self, *args):
  72. returncode = 0
  73. for i in xrange(self.retry_count):
  74. # If the previous run failed and a delay is configured, delay before the
  75. # next run.
  76. delay = self.computeDelay(i)
  77. if delay > 0:
  78. self.logger.info("Delaying for [%s second(s)] until next retry", delay)
  79. time.sleep(delay)
  80. self.logger.debug("Executing subprocess (%d/%d) with arguments: %s",
  81. (i+1), self.retry_count, args)
  82. returncode, _, stderr = self.execute(*args)
  83. self.logger.debug("Process terminated with return code: %d", returncode)
  84. if returncode == 0:
  85. break
  86. if not self.shouldRetry(stderr):
  87. self.logger.error("Process failure was not known to be transient; "
  88. "terminating with return code %d", returncode)
  89. break
  90. return returncode
  91. def main(args):
  92. parser = optparse.OptionParser()
  93. parser.disable_interspersed_args()
  94. parser.add_option('-v', '--verbose',
  95. action='count', default=0,
  96. help="Increase verbosity; can be specified multiple times")
  97. parser.add_option('-c', '--retry-count', metavar='COUNT',
  98. type=int, default=GitRetry.DEFAULT_RETRY_COUNT,
  99. help="Number of times to retry (default=%default)")
  100. parser.add_option('-d', '--delay', metavar='SECONDS',
  101. type=float, default=GitRetry.DEFAULT_DELAY_SECS,
  102. help="Specifies the amount of time (in seconds) to wait "
  103. "between successive retries (default=%default). This "
  104. "can be zero.")
  105. parser.add_option('-D', '--delay-factor', metavar='FACTOR',
  106. type=int, default=2,
  107. help="The exponential factor to apply to delays in between "
  108. "successive failures (default=%default). If this is "
  109. "zero, delays will increase linearly. Set this to "
  110. "one to have a constant (non-increasing) delay.")
  111. opts, args = parser.parse_args(args)
  112. # Configure logging verbosity
  113. if opts.verbose == 0:
  114. logging.getLogger().setLevel(logging.WARNING)
  115. elif opts.verbose == 1:
  116. logging.getLogger().setLevel(logging.INFO)
  117. else:
  118. logging.getLogger().setLevel(logging.DEBUG)
  119. # Execute retries
  120. retry = GitRetry(
  121. retry_count=opts.retry_count,
  122. delay=opts.delay,
  123. delay_factor=opts.delay_factor,
  124. )
  125. return retry(*args)
  126. if __name__ == '__main__':
  127. logging.basicConfig()
  128. logging.getLogger().setLevel(logging.WARNING)
  129. sys.exit(main(sys.argv[2:]))