git_retry.py 6.3 KB

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