Commit f2d7d6b5 authored by szager@chromium.org's avatar szager@chromium.org

Consolidate subprocess retry logic into gclient_utils.

Add retry for all git operations that speak to a remote.  This
should smooth out issues with the git/gerrit-on-borg service.

Review URL: https://codereview.chromium.org/26234004

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@229219 0039d316-1c4b-4281-b951-d872f2087c98
parent 8dce2c76
...@@ -281,7 +281,7 @@ class GitWrapper(SCMWrapper): ...@@ -281,7 +281,7 @@ class GitWrapper(SCMWrapper):
fetch_cmd = [ fetch_cmd = [
'-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin', '--prune'] '-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin', '--prune']
self._Run(fetch_cmd + quiet, options) self._Run(fetch_cmd + quiet, options, retry=True)
self._Run(['reset', '--hard', revision] + quiet, options) self._Run(['reset', '--hard', revision] + quiet, options)
self.UpdateSubmoduleConfig() self.UpdateSubmoduleConfig()
if file_list is not None: if file_list is not None:
...@@ -802,7 +802,7 @@ class GitWrapper(SCMWrapper): ...@@ -802,7 +802,7 @@ class GitWrapper(SCMWrapper):
self._Run(cmd + [url, folder], self._Run(cmd + [url, folder],
options, git_filter=True, filter_fn=filter_fn, options, git_filter=True, filter_fn=filter_fn,
cwd=self.cache_dir) cwd=self.cache_dir, retry=True)
else: else:
# For now, assert that host/path/to/repo.git is identical. We may want # For now, assert that host/path/to/repo.git is identical. We may want
# to relax this restriction in the future to allow for smarter cache # to relax this restriction in the future to allow for smarter cache
...@@ -819,7 +819,8 @@ class GitWrapper(SCMWrapper): ...@@ -819,7 +819,8 @@ class GitWrapper(SCMWrapper):
# Would normally use `git remote update`, but it doesn't support # Would normally use `git remote update`, but it doesn't support
# --progress, so use fetch instead. # --progress, so use fetch instead.
self._Run(['fetch'] + v + ['--multiple', '--progress', '--all'], self._Run(['fetch'] + v + ['--multiple', '--progress', '--all'],
options, git_filter=True, filter_fn=filter_fn, cwd=folder) options, git_filter=True, filter_fn=filter_fn, cwd=folder,
retry=True)
# If the clone has an object dependency on the existing repo, break it # If the clone has an object dependency on the existing repo, break it
# with repack and remove the linkage. # with repack and remove the linkage.
...@@ -858,16 +859,8 @@ class GitWrapper(SCMWrapper): ...@@ -858,16 +859,8 @@ class GitWrapper(SCMWrapper):
dir=parent_dir) dir=parent_dir)
try: try:
clone_cmd.append(tmp_dir) clone_cmd.append(tmp_dir)
for i in xrange(3): self._Run(clone_cmd, options, cwd=self._root_dir, git_filter=True,
try: retry=True)
self._Run(clone_cmd, options, cwd=self._root_dir, git_filter=True)
break
except subprocess2.CalledProcessError as e:
gclient_utils.rmtree(os.path.join(tmp_dir, '.git'))
if e.returncode != 128 or i == 2:
raise
print(str(e))
print('Retrying...')
gclient_utils.safe_makedirs(self.checkout_path) gclient_utils.safe_makedirs(self.checkout_path)
gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'), gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
os.path.join(self.checkout_path, '.git')) os.path.join(self.checkout_path, '.git'))
...@@ -1034,24 +1027,15 @@ class GitWrapper(SCMWrapper): ...@@ -1034,24 +1027,15 @@ class GitWrapper(SCMWrapper):
def _UpdateBranchHeads(self, options, fetch=False): def _UpdateBranchHeads(self, options, fetch=False):
"""Adds, and optionally fetches, "branch-heads" refspecs if requested.""" """Adds, and optionally fetches, "branch-heads" refspecs if requested."""
if hasattr(options, 'with_branch_heads') and options.with_branch_heads: if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
backoff_time = 5 config_cmd = ['config', 'remote.origin.fetch',
for _ in range(3): '+refs/branch-heads/*:refs/remotes/branch-heads/*',
try: '^\\+refs/branch-heads/\\*:.*$']
config_cmd = ['config', 'remote.origin.fetch', self._Run(config_cmd, options)
'+refs/branch-heads/*:refs/remotes/branch-heads/*', if fetch:
'^\\+refs/branch-heads/\\*:.*$'] fetch_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin']
self._Run(config_cmd, options) if options.verbose:
if fetch: fetch_cmd.append('--verbose')
fetch_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin'] self._Run(fetch_cmd, options, retry=True)
if options.verbose:
fetch_cmd.append('--verbose')
self._Run(fetch_cmd, options)
break
except subprocess2.CalledProcessError, e:
print(str(e))
print('Retrying in %.1f seconds...' % backoff_time)
time.sleep(backoff_time)
backoff_time *= 1.3
def _Run(self, args, _options, git_filter=False, **kwargs): def _Run(self, args, _options, git_filter=False, **kwargs):
kwargs.setdefault('cwd', self.checkout_path) kwargs.setdefault('cwd', self.checkout_path)
......
...@@ -21,6 +21,10 @@ import urlparse ...@@ -21,6 +21,10 @@ import urlparse
import subprocess2 import subprocess2
RETRY_MAX = 3
RETRY_INITIAL_SLEEP = 0.5
class Error(Exception): class Error(Exception):
"""gclient exception class.""" """gclient exception class."""
def __init__(self, msg, *args, **kwargs): def __init__(self, msg, *args, **kwargs):
...@@ -407,7 +411,7 @@ class GClientChildren(object): ...@@ -407,7 +411,7 @@ class GClientChildren(object):
def CheckCallAndFilter(args, stdout=None, filter_fn=None, def CheckCallAndFilter(args, stdout=None, filter_fn=None,
print_stdout=None, call_filter_on_first_line=False, print_stdout=None, call_filter_on_first_line=False,
**kwargs): retry=False, **kwargs):
"""Runs a command and calls back a filter function if needed. """Runs a command and calls back a filter function if needed.
Accepts all subprocess2.Popen() parameters plus: Accepts all subprocess2.Popen() parameters plus:
...@@ -416,62 +420,74 @@ def CheckCallAndFilter(args, stdout=None, filter_fn=None, ...@@ -416,62 +420,74 @@ def CheckCallAndFilter(args, stdout=None, filter_fn=None,
of the subprocess2's output. Each line has the trailing newline of the subprocess2's output. Each line has the trailing newline
character trimmed. character trimmed.
stdout: Can be any bufferable output. stdout: Can be any bufferable output.
retry: If the process exits non-zero, sleep for a brief interval and try
again, up to RETRY_MAX times.
stderr is always redirected to stdout. stderr is always redirected to stdout.
""" """
assert print_stdout or filter_fn assert print_stdout or filter_fn
stdout = stdout or sys.stdout stdout = stdout or sys.stdout
filter_fn = filter_fn or (lambda x: None) filter_fn = filter_fn or (lambda x: None)
kid = subprocess2.Popen(
args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
**kwargs)
GClientChildren.add(kid) sleep_interval = RETRY_INITIAL_SLEEP
run_cwd = kwargs.get('cwd', os.getcwd())
for _ in xrange(RETRY_MAX + 1):
kid = subprocess2.Popen(
args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
**kwargs)
# Do a flush of stdout before we begin reading from the subprocess2's stdout GClientChildren.add(kid)
stdout.flush()
# Also, we need to forward stdout to prevent weird re-ordering of output. # Do a flush of stdout before we begin reading from the subprocess2's stdout
# This has to be done on a per byte basis to make sure it is not buffered: stdout.flush()
# normally buffering is done for each line, but if svn requests input, no
# end-of-line character is output after the prompt and it would not show up. # Also, we need to forward stdout to prevent weird re-ordering of output.
try: # This has to be done on a per byte basis to make sure it is not buffered:
in_byte = kid.stdout.read(1) # normally buffering is done for each line, but if svn requests input, no
if in_byte: # end-of-line character is output after the prompt and it would not show up.
if call_filter_on_first_line: try:
filter_fn(None) in_byte = kid.stdout.read(1)
in_line = '' if in_byte:
while in_byte: if call_filter_on_first_line:
if in_byte != '\r': filter_fn(None)
if print_stdout: in_line = ''
stdout.write(in_byte) while in_byte:
if in_byte != '\n': if in_byte != '\r':
in_line += in_byte if print_stdout:
stdout.write(in_byte)
if in_byte != '\n':
in_line += in_byte
else:
filter_fn(in_line)
in_line = ''
else: else:
filter_fn(in_line) filter_fn(in_line)
in_line = '' in_line = ''
else: in_byte = kid.stdout.read(1)
# Flush the rest of buffered output. This is only an issue with
# stdout/stderr not ending with a \n.
if len(in_line):
filter_fn(in_line) filter_fn(in_line)
in_line = '' rv = kid.wait()
in_byte = kid.stdout.read(1)
# Flush the rest of buffered output. This is only an issue with # Don't put this in a 'finally,' since the child may still run if we get
# stdout/stderr not ending with a \n. # an exception.
if len(in_line): GClientChildren.remove(kid)
filter_fn(in_line)
rv = kid.wait() except KeyboardInterrupt:
print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
# Don't put this in a 'finally,' since the child may still run if we get an raise
# exception.
GClientChildren.remove(kid) if rv == 0:
return 0
except KeyboardInterrupt: if not retry:
print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args) break
raise print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
if rv: sys.sleep(sleep_interval)
raise subprocess2.CalledProcessError( sleep_interval *= 2
rv, args, kwargs.get('cwd', None), None, None) raise subprocess2.CalledProcessError(
return 0 rv, args, kwargs.get('cwd', None), None, None)
def FindGclientRoot(from_dir, filename='.gclient'): def FindGclientRoot(from_dir, filename='.gclient'):
......
...@@ -34,8 +34,8 @@ class GclientUtilsUnittest(GclientUtilBase): ...@@ -34,8 +34,8 @@ class GclientUtilsUnittest(GclientUtilBase):
'GetGClientRootAndEntries', 'GetEditor', 'IsDateRevision', 'GetGClientRootAndEntries', 'GetEditor', 'IsDateRevision',
'MakeDateRevision', 'MakeFileAutoFlush', 'MakeFileAnnotated', 'MakeDateRevision', 'MakeFileAutoFlush', 'MakeFileAnnotated',
'PathDifference', 'ParseCodereviewSettingsContent', 'NumLocalCpus', 'PathDifference', 'ParseCodereviewSettingsContent', 'NumLocalCpus',
'PrintableObject', 'RunEditor', 'GCLIENT_CHILDREN', 'PrintableObject', 'RETRY_INITIAL_SLEEP', 'RETRY_MAX', 'RunEditor',
'GCLIENT_CHILDREN_LOCK', 'GClientChildren', 'GCLIENT_CHILDREN', 'GCLIENT_CHILDREN_LOCK', 'GClientChildren',
'SplitUrlRevision', 'SyntaxErrorToError', 'UpgradeToHttps', 'Wrapper', 'SplitUrlRevision', 'SyntaxErrorToError', 'UpgradeToHttps', 'Wrapper',
'WorkItem', 'codecs', 'lockedmethod', 'logging', 'os', 'pipes', 'Queue', 'WorkItem', 'codecs', 'lockedmethod', 'logging', 'os', 'pipes', 'Queue',
're', 'rmtree', 'safe_makedirs', 'safe_rename', 'stat', 'subprocess', 're', 'rmtree', 'safe_makedirs', 'safe_rename', 'stat', 'subprocess',
...@@ -51,8 +51,9 @@ class CheckCallAndFilterTestCase(GclientUtilBase): ...@@ -51,8 +51,9 @@ class CheckCallAndFilterTestCase(GclientUtilBase):
def __init__(self, test_string): def __init__(self, test_string):
self.stdout = StringIO.StringIO(test_string) self.stdout = StringIO.StringIO(test_string)
self.pid = 9284 self.pid = 9284
# pylint: disable=R0201
def wait(self): def wait(self):
pass return 0
def _inner(self, args, test_string): def _inner(self, args, test_string):
cwd = 'bleh' cwd = 'bleh'
...@@ -68,6 +69,7 @@ class CheckCallAndFilterTestCase(GclientUtilBase): ...@@ -68,6 +69,7 @@ class CheckCallAndFilterTestCase(GclientUtilBase):
stderr=subprocess2.STDOUT, stderr=subprocess2.STDOUT,
bufsize=0).AndReturn(self.ProcessIdMock(test_string)) bufsize=0).AndReturn(self.ProcessIdMock(test_string))
os.getcwd()
self.mox.ReplayAll() self.mox.ReplayAll()
compiled_pattern = gclient_utils.re.compile(r'a(.*)b') compiled_pattern = gclient_utils.re.compile(r'a(.*)b')
line_list = [] line_list = []
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment