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