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