blob: f8db8b518a3dc43c3c8c20a31beae0252038e9dc [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.
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00005"""Generic retry wrapper for Git operations.
Dan Jacques2f8b0c12017-04-05 12:57:21 -07006
7This is largely DEPRECATED in favor of the Infra Git wrapper:
Josip Sokcevic9c0dc302020-11-20 18:41:25 +00008https://chromium.googlesource.com/infra/infra/+/HEAD/go/src/infra/tools/git
Dan Jacques2f8b0c12017-04-05 12:57:21 -07009"""
10
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000011import logging
12import optparse
Dan Jacques2f8b0c12017-04-05 12:57:21 -070013import os
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000014import subprocess
15import sys
16import threading
17import time
18
19from git_common import GIT_EXE, GIT_TRANSIENT_ERRORS_RE
20
21
22class TeeThread(threading.Thread):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000023 def __init__(self, fd, out_fd, name):
24 super(TeeThread, self).__init__(name='git-retry.tee.%s' % (name, ))
25 self.data = None
26 self.fd = fd
27 self.out_fd = out_fd
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000028
Mike Frysinger124bb8e2023-09-06 05:48:55 +000029 def run(self):
30 chunks = []
31 for line in self.fd:
32 line = line.decode('utf-8')
33 chunks.append(line)
34 self.out_fd.write(line)
35 self.data = ''.join(chunks)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000036
37
38class GitRetry(object):
39
Mike Frysinger124bb8e2023-09-06 05:48:55 +000040 logger = logging.getLogger('git-retry')
41 DEFAULT_DELAY_SECS = 3.0
42 DEFAULT_RETRY_COUNT = 5
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000043
Mike Frysinger124bb8e2023-09-06 05:48:55 +000044 def __init__(self, retry_count=None, delay=None, delay_factor=None):
45 self.retry_count = retry_count or self.DEFAULT_RETRY_COUNT
46 self.delay = max(delay, 0) if delay else 0
47 self.delay_factor = max(delay_factor, 0) if delay_factor else 0
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000048
Mike Frysinger124bb8e2023-09-06 05:48:55 +000049 def shouldRetry(self, stderr):
50 m = GIT_TRANSIENT_ERRORS_RE.search(stderr)
51 if not m:
52 return False
53 self.logger.info("Encountered known transient error: [%s]",
54 stderr[m.start():m.end()])
55 return True
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000056
Mike Frysinger124bb8e2023-09-06 05:48:55 +000057 @staticmethod
58 def execute(*args):
59 args = (GIT_EXE, ) + args
60 proc = subprocess.Popen(
61 args,
62 stderr=subprocess.PIPE,
63 )
64 stderr_tee = TeeThread(proc.stderr, sys.stderr, 'stderr')
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000065
Mike Frysinger124bb8e2023-09-06 05:48:55 +000066 # Start our process. Collect/tee 'stdout' and 'stderr'.
67 stderr_tee.start()
68 try:
69 proc.wait()
70 except KeyboardInterrupt:
71 proc.kill()
72 raise
73 finally:
74 stderr_tee.join()
75 return proc.returncode, None, stderr_tee.data
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000076
Mike Frysinger124bb8e2023-09-06 05:48:55 +000077 def computeDelay(self, iteration):
78 """Returns: the delay (in seconds) for a given iteration
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000079
80 The first iteration has a delay of '0'.
81
82 Args:
83 iteration: (int) The iteration index (starting with zero as the first
84 iteration)
85 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000086 if (not self.delay) or (iteration == 0):
87 return 0
88 if self.delay_factor == 0:
89 # Linear delay
90 return iteration * self.delay
91 # Exponential delay
92 return (self.delay_factor**(iteration - 1)) * self.delay
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000093
Mike Frysinger124bb8e2023-09-06 05:48:55 +000094 def __call__(self, *args):
95 returncode = 0
96 for i in range(self.retry_count):
97 # If the previous run failed and a delay is configured, delay before
98 # the next run.
99 delay = self.computeDelay(i)
100 if delay > 0:
101 self.logger.info("Delaying for [%s second(s)] until next retry",
102 delay)
103 time.sleep(delay)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000104
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000105 self.logger.debug("Executing subprocess (%d/%d) with arguments: %s",
106 (i + 1), self.retry_count, args)
107 returncode, _, stderr = self.execute(*args)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000108
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000109 self.logger.debug("Process terminated with return code: %d",
110 returncode)
111 if returncode == 0:
112 break
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000113
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000114 if not self.shouldRetry(stderr):
115 self.logger.error(
116 "Process failure was not known to be transient; "
117 "terminating with return code %d", returncode)
118 break
119 return returncode
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000120
121
122def main(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000123 # If we're using the Infra Git wrapper, do nothing here.
124 # https://chromium.googlesource.com/infra/infra/+/HEAD/go/src/infra/tools/git
125 if 'INFRA_GIT_WRAPPER' in os.environ:
126 # Remove Git's execution path from PATH so that our call-through
127 # re-invokes the Git wrapper. 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 ])
134 return subprocess.call([GIT_EXE] + args, env=env)
Dan Jacques2f8b0c12017-04-05 12:57:21 -0700135
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000136 parser = optparse.OptionParser()
137 parser.disable_interspersed_args()
138 parser.add_option(
139 '-v',
140 '--verbose',
141 action='count',
142 default=0,
143 help="Increase verbosity; can be specified multiple times")
144 parser.add_option('-c',
145 '--retry-count',
146 metavar='COUNT',
147 type=int,
148 default=GitRetry.DEFAULT_RETRY_COUNT,
149 help="Number of times to retry (default=%default)")
150 parser.add_option('-d',
151 '--delay',
152 metavar='SECONDS',
153 type=float,
154 default=GitRetry.DEFAULT_DELAY_SECS,
155 help="Specifies the amount of time (in seconds) to wait "
156 "between successive retries (default=%default). This "
157 "can be zero.")
158 parser.add_option(
159 '-D',
160 '--delay-factor',
161 metavar='FACTOR',
162 type=int,
163 default=2,
164 help="The exponential factor to apply to delays in between "
165 "successive failures (default=%default). If this is "
166 "zero, delays will increase linearly. Set this to "
167 "one to have a constant (non-increasing) delay.")
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000168
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000169 opts, args = parser.parse_args(args)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000170
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000171 # Configure logging verbosity
172 if opts.verbose == 0:
173 logging.getLogger().setLevel(logging.WARNING)
174 elif opts.verbose == 1:
175 logging.getLogger().setLevel(logging.INFO)
176 else:
177 logging.getLogger().setLevel(logging.DEBUG)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000178
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 # Execute retries
180 retry = GitRetry(
181 retry_count=opts.retry_count,
182 delay=opts.delay,
183 delay_factor=opts.delay_factor,
184 )
185 return retry(*args)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000186
187
188if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000189 logging.basicConfig()
190 logging.getLogger().setLevel(logging.WARNING)
191 try:
192 sys.exit(main(sys.argv[2:]))
193 except KeyboardInterrupt:
194 sys.stderr.write('interrupted\n')
195 sys.exit(1)