Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Side by Side Diff: git_cache.py

Issue 2497503002: Add retries to file operations for Windows. (Closed)
Patch Set: Better formatting string. Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """A git command for managing a local cache of git repositories.""" 6 """A git command for managing a local cache of git repositories."""
7 7
8 from __future__ import print_function 8 from __future__ import print_function
9 import errno 9 import errno
10 import logging 10 import logging
(...skipping 23 matching lines...) Expand all
34 except NameError: 34 except NameError:
35 class WinErr(Exception): 35 class WinErr(Exception):
36 pass 36 pass
37 37
38 class LockError(Exception): 38 class LockError(Exception):
39 pass 39 pass
40 40
41 class ClobberNeeded(Exception): 41 class ClobberNeeded(Exception):
42 pass 42 pass
43 43
44
45 def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
46 sleep_time=0.25, printerr=None):
47 """Executes |fn| up to |count| times, backing off exponentially.
48
49 Args:
50 fn (callable): The function to execute. If this raises a handled
51 exception, the function will retry with exponential backoff.
52 excs (tuple): A tuple of Exception types to handle. If one of these is
53 raised by |fn|, a retry will be attempted. If |fn| raises an Exception
54 that is not in this list, it will immediately pass through. If |excs|
55 is empty, the Exception base class will be used.
56 name (str): Optional operation name to print in the retry string.
57 count (int): The number of times to try before allowing the exception to
58 pass through.
59 sleep_time (float): The initial number of seconds to sleep in between
60 retries. This will be doubled each retry.
61 printerr (callable): Function that will be called with the error string upon
62 failures. If None, |logging.warning| will be used.
63
64 Returns: The return value of the successful fn.
65 """
66 printerr = printerr or logging.warning
67 for i in xrange(count):
68 try:
69 return fn()
70 except excs as e:
71 if (i+1) >= count:
72 raise
73
74 printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % (
75 (name or 'operation'), sleep_time, (i+1), count, e))
76 time.sleep(sleep_time)
77 sleep_time *= 2
78
79
44 class Lockfile(object): 80 class Lockfile(object):
45 """Class to represent a cross-platform process-specific lockfile.""" 81 """Class to represent a cross-platform process-specific lockfile."""
46 82
47 def __init__(self, path, timeout=0): 83 def __init__(self, path, timeout=0):
48 self.path = os.path.abspath(path) 84 self.path = os.path.abspath(path)
49 self.timeout = timeout 85 self.timeout = timeout
50 self.lockfile = self.path + ".lock" 86 self.lockfile = self.path + ".lock"
51 self.pid = os.getpid() 87 self.pid = os.getpid()
52 88
53 def _read_pid(self): 89 def _read_pid(self):
(...skipping 18 matching lines...) Expand all
72 f.close() 108 f.close()
73 109
74 def _remove_lockfile(self): 110 def _remove_lockfile(self):
75 """Delete the lockfile. Complains (implicitly) if it doesn't exist. 111 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
76 112
77 See gclient_utils.py:rmtree docstring for more explanation on the 113 See gclient_utils.py:rmtree docstring for more explanation on the
78 windows case. 114 windows case.
79 """ 115 """
80 if sys.platform == 'win32': 116 if sys.platform == 'win32':
81 lockfile = os.path.normcase(self.lockfile) 117 lockfile = os.path.normcase(self.lockfile)
82 for _ in xrange(3): 118
119 def delete():
83 exitcode = subprocess.call(['cmd.exe', '/c', 120 exitcode = subprocess.call(['cmd.exe', '/c',
84 'del', '/f', '/q', lockfile]) 121 'del', '/f', '/q', lockfile])
85 if exitcode == 0: 122 if exitcode != 0:
86 return 123 raise LockError('Failed to remove lock: %s' % (lockfile,))
87 time.sleep(3) 124 exponential_backoff_retry(
88 raise LockError('Failed to remove lock: %s' % lockfile) 125 delete,
126 excs=(LockError,),
127 name='del [%s]' % (lockfile,))
89 else: 128 else:
90 os.remove(self.lockfile) 129 os.remove(self.lockfile)
91 130
92 def lock(self): 131 def lock(self):
93 """Acquire the lock. 132 """Acquire the lock.
94 133
95 This will block with a deadline of self.timeout seconds. 134 This will block with a deadline of self.timeout seconds.
96 """ 135 """
97 elapsed = 0 136 elapsed = 0
98 while True: 137 while True:
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after
174 self.url = url 213 self.url = url
175 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])]) 214 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
176 self.basedir = self.UrlToCacheDir(url) 215 self.basedir = self.UrlToCacheDir(url)
177 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir) 216 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
178 if print_func: 217 if print_func:
179 self.print = self.print_without_file 218 self.print = self.print_without_file
180 self.print_func = print_func 219 self.print_func = print_func
181 else: 220 else:
182 self.print = print 221 self.print = print
183 222
184 def print_without_file(self, message, **kwargs): 223 def print_without_file(self, message, **_kwargs):
185 self.print_func(message) 224 self.print_func(message)
186 225
187 @property 226 @property
188 def bootstrap_bucket(self): 227 def bootstrap_bucket(self):
189 if 'chrome-internal' in self.url: 228 if 'chrome-internal' in self.url:
190 return 'chrome-git-cache' 229 return 'chrome-git-cache'
191 else: 230 else:
192 return 'chromium-git-cache' 231 return 'chromium-git-cache'
193 232
194 @classmethod 233 @classmethod
(...skipping 28 matching lines...) Expand all
223 cachepath = subprocess.check_output( 262 cachepath = subprocess.check_output(
224 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip() 263 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
225 except subprocess.CalledProcessError: 264 except subprocess.CalledProcessError:
226 cachepath = None 265 cachepath = None
227 if not cachepath: 266 if not cachepath:
228 raise RuntimeError( 267 raise RuntimeError(
229 'No global cache.cachepath git configuration found.') 268 'No global cache.cachepath git configuration found.')
230 setattr(cls, 'cachepath', cachepath) 269 setattr(cls, 'cachepath', cachepath)
231 return getattr(cls, 'cachepath') 270 return getattr(cls, 'cachepath')
232 271
272 def Rename(self, src, dst):
273 # This is somehow racy on Windows.
274 # Catching OSError because WindowsError isn't portable and
275 # pylint complains.
276 exponential_backoff_retry(
277 lambda: os.rename(src, dst),
278 excs=(OSError,),
279 name='rename [%s] => [%s]' % (src, dst),
280 printerr=self.print)
281
233 def RunGit(self, cmd, **kwargs): 282 def RunGit(self, cmd, **kwargs):
234 """Run git in a subprocess.""" 283 """Run git in a subprocess."""
235 cwd = kwargs.setdefault('cwd', self.mirror_path) 284 cwd = kwargs.setdefault('cwd', self.mirror_path)
236 kwargs.setdefault('print_stdout', False) 285 kwargs.setdefault('print_stdout', False)
237 kwargs.setdefault('filter_fn', self.print) 286 kwargs.setdefault('filter_fn', self.print)
238 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy()) 287 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
239 env.setdefault('GIT_ASKPASS', 'true') 288 env.setdefault('GIT_ASKPASS', 'true')
240 env.setdefault('SSH_ASKPASS', 'true') 289 env.setdefault('SSH_ASKPASS', 'true')
241 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd)) 290 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
242 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs) 291 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
317 with zipfile.ZipFile(filename, 'r') as f: 366 with zipfile.ZipFile(filename, 'r') as f:
318 f.printdir() 367 f.printdir()
319 f.extractall(directory) 368 f.extractall(directory)
320 except Exception as e: 369 except Exception as e:
321 self.print('Encountered error: %s' % str(e), file=sys.stderr) 370 self.print('Encountered error: %s' % str(e), file=sys.stderr)
322 retcode = 1 371 retcode = 1
323 else: 372 else:
324 retcode = 0 373 retcode = 0
325 finally: 374 finally:
326 # Clean up the downloaded zipfile. 375 # Clean up the downloaded zipfile.
327 gclient_utils.rm_file_or_tree(tempdir) 376 #
377 # This is somehow racy on Windows.
378 # Catching OSError because WindowsError isn't portable and
379 # pylint complains.
380 exponential_backoff_retry(
381 lambda: gclient_utils.rm_file_or_tree(tempdir),
382 excs=(OSError,),
383 name='rmtree [%s]' % (tempdir,),
384 printerr=self.print)
328 385
329 if retcode: 386 if retcode:
330 self.print( 387 self.print(
331 'Extracting bootstrap zipfile %s failed.\n' 388 'Extracting bootstrap zipfile %s failed.\n'
332 'Resuming normal operations.' % filename) 389 'Resuming normal operations.' % filename)
333 return False 390 return False
334 return True 391 return True
335 392
336 def exists(self): 393 def exists(self):
337 return os.path.isfile(os.path.join(self.mirror_path, 'config')) 394 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
(...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after
434 # This is a major failure, we need to clean and force a bootstrap. 491 # This is a major failure, we need to clean and force a bootstrap.
435 gclient_utils.rmtree(rundir) 492 gclient_utils.rmtree(rundir)
436 self.print(GIT_CACHE_CORRUPT_MESSAGE) 493 self.print(GIT_CACHE_CORRUPT_MESSAGE)
437 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True) 494 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
438 assert tempdir 495 assert tempdir
439 self._fetch(tempdir or self.mirror_path, verbose, depth) 496 self._fetch(tempdir or self.mirror_path, verbose, depth)
440 finally: 497 finally:
441 if tempdir: 498 if tempdir:
442 if os.path.exists(self.mirror_path): 499 if os.path.exists(self.mirror_path):
443 gclient_utils.rmtree(self.mirror_path) 500 gclient_utils.rmtree(self.mirror_path)
444 os.rename(tempdir, self.mirror_path) 501 self.Rename(tempdir, self.mirror_path)
445 if not ignore_lock: 502 if not ignore_lock:
446 lockfile.unlock() 503 lockfile.unlock()
447 504
448 def update_bootstrap(self, prune=False): 505 def update_bootstrap(self, prune=False):
449 # The files are named <git number>.zip 506 # The files are named <git number>.zip
450 gen_number = subprocess.check_output( 507 gen_number = subprocess.check_output(
451 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip() 508 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
452 # Run Garbage Collect to compress packfile. 509 # Run Garbage Collect to compress packfile.
453 self.RunGit(['gc', '--prune=all']) 510 self.RunGit(['gc', '--prune=all'])
454 # Creating a temp file and then deleting it ensures we can use this name. 511 # Creating a temp file and then deleting it ensures we can use this name.
(...skipping 267 matching lines...) Expand 10 before | Expand all | Expand 10 after
722 dispatcher = subcommand.CommandDispatcher(__name__) 779 dispatcher = subcommand.CommandDispatcher(__name__)
723 return dispatcher.execute(OptionParser(), argv) 780 return dispatcher.execute(OptionParser(), argv)
724 781
725 782
726 if __name__ == '__main__': 783 if __name__ == '__main__':
727 try: 784 try:
728 sys.exit(main(sys.argv[1:])) 785 sys.exit(main(sys.argv[1:]))
729 except KeyboardInterrupt: 786 except KeyboardInterrupt:
730 sys.stderr.write('interrupted\n') 787 sys.stderr.write('interrupted\n')
731 sys.exit(1) 788 sys.exit(1)
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698