Josip Sokcevic | 4de5dea | 2022-03-23 21:15:14 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 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. |
Josip Sokcevic | 4de5dea | 2022-03-23 21:15:14 +0000 | [diff] [blame] | 5 | """Generic retry wrapper for Git operations. |
Dan Jacques | 2f8b0c1 | 2017-04-05 12:57:21 -0700 | [diff] [blame] | 6 | |
| 7 | This is largely DEPRECATED in favor of the Infra Git wrapper: |
Josip Sokcevic | 9c0dc30 | 2020-11-20 18:41:25 +0000 | [diff] [blame] | 8 | https://chromium.googlesource.com/infra/infra/+/HEAD/go/src/infra/tools/git |
Dan Jacques | 2f8b0c1 | 2017-04-05 12:57:21 -0700 | [diff] [blame] | 9 | """ |
| 10 | |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 11 | import logging |
| 12 | import optparse |
Dan Jacques | 2f8b0c1 | 2017-04-05 12:57:21 -0700 | [diff] [blame] | 13 | import os |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 14 | import subprocess |
| 15 | import sys |
| 16 | import threading |
| 17 | import time |
| 18 | |
| 19 | from git_common import GIT_EXE, GIT_TRANSIENT_ERRORS_RE |
| 20 | |
| 21 | |
| 22 | class TeeThread(threading.Thread): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 23 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 28 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 29 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 36 | |
| 37 | |
| 38 | class GitRetry(object): |
| 39 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 40 | logger = logging.getLogger('git-retry') |
| 41 | DEFAULT_DELAY_SECS = 3.0 |
| 42 | DEFAULT_RETRY_COUNT = 5 |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 43 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 44 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 48 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 49 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 56 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 57 | @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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 65 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 66 | # 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 76 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 77 | def computeDelay(self, iteration): |
| 78 | """Returns: the delay (in seconds) for a given iteration |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 79 | |
| 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 Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 86 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 93 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 94 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 104 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 105 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 108 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 109 | self.logger.debug("Process terminated with return code: %d", |
| 110 | returncode) |
| 111 | if returncode == 0: |
| 112 | break |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 113 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 114 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 120 | |
| 121 | |
| 122 | def main(args): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 123 | # 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 Jacques | 2f8b0c1 | 2017-04-05 12:57:21 -0700 | [diff] [blame] | 135 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 136 | 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 168 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 169 | opts, args = parser.parse_args(args) |
dnj@chromium.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 170 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 171 | # 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 178 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 179 | # 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.org | de219ec | 2014-07-28 17:39:08 +0000 | [diff] [blame] | 186 | |
| 187 | |
| 188 | if __name__ == '__main__': |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame^] | 189 | 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) |