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

Side by Side Diff: git_cache.py

Issue 1650993005: Allow blocking git-cache update with a timeout. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: nit Created 4 years, 10 months 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 | Annotate | Revision Log
« no previous file with comments | « gclient_scm.py ('k') | 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 26 matching lines...) Expand all
37 37
38 class LockError(Exception): 38 class LockError(Exception):
39 pass 39 pass
40 40
41 class RefsHeadsFailedToFetch(Exception): 41 class RefsHeadsFailedToFetch(Exception):
42 pass 42 pass
43 43
44 class Lockfile(object): 44 class Lockfile(object):
45 """Class to represent a cross-platform process-specific lockfile.""" 45 """Class to represent a cross-platform process-specific lockfile."""
46 46
47 def __init__(self, path): 47 def __init__(self, path, timeout=0):
48 self.path = os.path.abspath(path) 48 self.path = os.path.abspath(path)
49 self.timeout = timeout
49 self.lockfile = self.path + ".lock" 50 self.lockfile = self.path + ".lock"
50 self.pid = os.getpid() 51 self.pid = os.getpid()
51 52
52 def _read_pid(self): 53 def _read_pid(self):
53 """Read the pid stored in the lockfile. 54 """Read the pid stored in the lockfile.
54 55
55 Note: This method is potentially racy. By the time it returns the lockfile 56 Note: This method is potentially racy. By the time it returns the lockfile
56 may have been unlocked, removed, or stolen by some other process. 57 may have been unlocked, removed, or stolen by some other process.
57 """ 58 """
58 try: 59 try:
(...skipping 25 matching lines...) Expand all
84 if exitcode == 0: 85 if exitcode == 0:
85 return 86 return
86 time.sleep(3) 87 time.sleep(3)
87 raise LockError('Failed to remove lock: %s' % lockfile) 88 raise LockError('Failed to remove lock: %s' % lockfile)
88 else: 89 else:
89 os.remove(self.lockfile) 90 os.remove(self.lockfile)
90 91
91 def lock(self): 92 def lock(self):
92 """Acquire the lock. 93 """Acquire the lock.
93 94
94 Note: This is a NON-BLOCKING FAIL-FAST operation. 95 This will block with a deadline of self.timeout seconds.
95 Do. Or do not. There is no try. 96 If self.timeout is zero, this is a NON-BLOCKING FAIL-FAST operation.
96 """ 97 """
97 try: 98 elapsed = 0
98 self._make_lockfile() 99 while True:
99 except OSError as e: 100 try:
100 if e.errno == errno.EEXIST: 101 self._make_lockfile()
101 raise LockError("%s is already locked" % self.path) 102 return
102 else: 103 except OSError as e:
103 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno)) 104 if elapsed < self.timeout:
105 sleep_time = min(3, self.timeout - elapsed)
106 logging.info('Could not create git cache lockfile; '
107 'will retry after sleep(%d).', sleep_time);
108 elapsed += sleep_time
109 time.sleep(sleep_time)
110 continue
111 if e.errno == errno.EEXIST:
112 raise LockError("%s is already locked" % self.path)
113 else:
114 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
104 115
105 def unlock(self): 116 def unlock(self):
106 """Release the lock.""" 117 """Release the lock."""
107 try: 118 try:
108 if not self.is_locked(): 119 if not self.is_locked():
109 raise LockError("%s is not locked" % self.path) 120 raise LockError("%s is not locked" % self.path)
110 if not self.i_am_locking(): 121 if not self.i_am_locking():
111 raise LockError("%s is locked, but not by me" % self.path) 122 raise LockError("%s is locked, but not by me" % self.path)
112 self._remove_lockfile() 123 self._remove_lockfile()
113 except WinErr: 124 except WinErr:
(...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after
394 for spec in fetch_specs: 405 for spec in fetch_specs:
395 try: 406 try:
396 self.print('Fetching %s' % spec) 407 self.print('Fetching %s' % spec)
397 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True) 408 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
398 except subprocess.CalledProcessError: 409 except subprocess.CalledProcessError:
399 if spec == '+refs/heads/*:refs/heads/*': 410 if spec == '+refs/heads/*:refs/heads/*':
400 raise RefsHeadsFailedToFetch 411 raise RefsHeadsFailedToFetch
401 logging.warn('Fetch of %s failed' % spec) 412 logging.warn('Fetch of %s failed' % spec)
402 413
403 def populate(self, depth=None, shallow=False, bootstrap=False, 414 def populate(self, depth=None, shallow=False, bootstrap=False,
404 verbose=False, ignore_lock=False): 415 verbose=False, ignore_lock=False, lock_timeout=0):
405 assert self.GetCachePath() 416 assert self.GetCachePath()
406 if shallow and not depth: 417 if shallow and not depth:
407 depth = 10000 418 depth = 10000
408 gclient_utils.safe_makedirs(self.GetCachePath()) 419 gclient_utils.safe_makedirs(self.GetCachePath())
409 420
410 lockfile = Lockfile(self.mirror_path) 421 lockfile = Lockfile(self.mirror_path, lock_timeout)
411 if not ignore_lock: 422 if not ignore_lock:
412 lockfile.lock() 423 lockfile.lock()
413 424
414 tempdir = None 425 tempdir = None
415 try: 426 try:
416 tempdir = self._ensure_bootstrapped(depth, bootstrap) 427 tempdir = self._ensure_bootstrapped(depth, bootstrap)
417 rundir = tempdir or self.mirror_path 428 rundir = tempdir or self.mirror_path
418 self._fetch(rundir, verbose, depth) 429 self._fetch(rundir, verbose, depth)
419 except RefsHeadsFailedToFetch: 430 except RefsHeadsFailedToFetch:
420 # This is a major failure, we need to clean and force a bootstrap. 431 # This is a major failure, we need to clean and force a bootstrap.
(...skipping 154 matching lines...) Expand 10 before | Expand all | Expand 10 after
575 if not len(args) == 1: 586 if not len(args) == 1:
576 parser.error('git cache populate only takes exactly one repo url.') 587 parser.error('git cache populate only takes exactly one repo url.')
577 url = args[0] 588 url = args[0]
578 589
579 mirror = Mirror(url, refs=options.ref) 590 mirror = Mirror(url, refs=options.ref)
580 kwargs = { 591 kwargs = {
581 'verbose': options.verbose, 592 'verbose': options.verbose,
582 'shallow': options.shallow, 593 'shallow': options.shallow,
583 'bootstrap': not options.no_bootstrap, 594 'bootstrap': not options.no_bootstrap,
584 'ignore_lock': options.ignore_locks, 595 'ignore_lock': options.ignore_locks,
596 'lock_timeout': options.timeout,
585 } 597 }
586 if options.depth: 598 if options.depth:
587 kwargs['depth'] = options.depth 599 kwargs['depth'] = options.depth
588 mirror.populate(**kwargs) 600 mirror.populate(**kwargs)
589 601
590 602
591 @subcommand.usage('Fetch new commits into cache and current checkout') 603 @subcommand.usage('Fetch new commits into cache and current checkout')
592 def CMDfetch(parser, args): 604 def CMDfetch(parser, args):
593 """Update mirror, and fetch in cwd.""" 605 """Update mirror, and fetch in cwd."""
594 parser.add_option('--all', action='store_true', help='Fetch all remotes') 606 parser.add_option('--all', action='store_true', help='Fetch all remotes')
(...skipping 23 matching lines...) Expand all
618 remotes = [upstream] 630 remotes = [upstream]
619 if not remotes: 631 if not remotes:
620 remotes = ['origin'] 632 remotes = ['origin']
621 633
622 cachepath = Mirror.GetCachePath() 634 cachepath = Mirror.GetCachePath()
623 git_dir = os.path.abspath(subprocess.check_output( 635 git_dir = os.path.abspath(subprocess.check_output(
624 [Mirror.git_exe, 'rev-parse', '--git-dir'])) 636 [Mirror.git_exe, 'rev-parse', '--git-dir']))
625 git_dir = os.path.abspath(git_dir) 637 git_dir = os.path.abspath(git_dir)
626 if git_dir.startswith(cachepath): 638 if git_dir.startswith(cachepath):
627 mirror = Mirror.FromPath(git_dir) 639 mirror = Mirror.FromPath(git_dir)
628 mirror.populate(bootstrap=not options.no_bootstrap) 640 mirror.populate(
641 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
629 return 0 642 return 0
630 for remote in remotes: 643 for remote in remotes:
631 remote_url = subprocess.check_output( 644 remote_url = subprocess.check_output(
632 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip() 645 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
633 if remote_url.startswith(cachepath): 646 if remote_url.startswith(cachepath):
634 mirror = Mirror.FromPath(remote_url) 647 mirror = Mirror.FromPath(remote_url)
635 mirror.print = lambda *args: None 648 mirror.print = lambda *args: None
636 print('Updating git cache...') 649 print('Updating git cache...')
637 mirror.populate(bootstrap=not options.no_bootstrap) 650 mirror.populate(
651 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
638 subprocess.check_call([Mirror.git_exe, 'fetch', remote]) 652 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
639 return 0 653 return 0
640 654
641 655
642 @subcommand.usage('[url of repo to unlock, or -a|--all]') 656 @subcommand.usage('[url of repo to unlock, or -a|--all]')
643 def CMDunlock(parser, args): 657 def CMDunlock(parser, args):
644 """Unlock one or all repos if their lock files are still around.""" 658 """Unlock one or all repos if their lock files are still around."""
645 parser.add_option('--force', '-f', action='store_true', 659 parser.add_option('--force', '-f', action='store_true',
646 help='Actually perform the action') 660 help='Actually perform the action')
647 parser.add_option('--all', '-a', action='store_true', 661 parser.add_option('--all', '-a', action='store_true',
(...skipping 28 matching lines...) Expand all
676 """Wrapper class for OptionParser to handle global options.""" 690 """Wrapper class for OptionParser to handle global options."""
677 691
678 def __init__(self, *args, **kwargs): 692 def __init__(self, *args, **kwargs):
679 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs) 693 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
680 self.add_option('-c', '--cache-dir', 694 self.add_option('-c', '--cache-dir',
681 help='Path to the directory containing the cache') 695 help='Path to the directory containing the cache')
682 self.add_option('-v', '--verbose', action='count', default=1, 696 self.add_option('-v', '--verbose', action='count', default=1,
683 help='Increase verbosity (can be passed multiple times)') 697 help='Increase verbosity (can be passed multiple times)')
684 self.add_option('-q', '--quiet', action='store_true', 698 self.add_option('-q', '--quiet', action='store_true',
685 help='Suppress all extraneous output') 699 help='Suppress all extraneous output')
700 self.add_option('--timeout', type='int', default=0,
701 help='Timeout for acquiring cache lock, in seconds')
686 702
687 def parse_args(self, args=None, values=None): 703 def parse_args(self, args=None, values=None):
688 options, args = optparse.OptionParser.parse_args(self, args, values) 704 options, args = optparse.OptionParser.parse_args(self, args, values)
689 if options.quiet: 705 if options.quiet:
690 options.verbose = 0 706 options.verbose = 0
691 707
692 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] 708 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
693 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)]) 709 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
694 710
695 try: 711 try:
(...skipping 14 matching lines...) Expand all
710 dispatcher = subcommand.CommandDispatcher(__name__) 726 dispatcher = subcommand.CommandDispatcher(__name__)
711 return dispatcher.execute(OptionParser(), argv) 727 return dispatcher.execute(OptionParser(), argv)
712 728
713 729
714 if __name__ == '__main__': 730 if __name__ == '__main__':
715 try: 731 try:
716 sys.exit(main(sys.argv[1:])) 732 sys.exit(main(sys.argv[1:]))
717 except KeyboardInterrupt: 733 except KeyboardInterrupt:
718 sys.stderr.write('interrupted\n') 734 sys.stderr.write('interrupted\n')
719 sys.exit(1) 735 sys.exit(1)
OLDNEW
« no previous file with comments | « gclient_scm.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698