git_cache: Remove locks

These aren't in use, and the original problem they were
meant to solve has been solved at the gclient.py layer 
using resource locking:
  https://codereview.chromium.org/2049583003

Bug: 773008
Change-Id: I6609f39d7f15604e0bb3d742a41c4f9fec87a57a
Reviewed-on: https://chromium-review.googlesource.com/707728
Reviewed-by: Aaron Gable <agable@chromium.org>
Reviewed-by: Robbie Iannucci <iannucci@chromium.org>
Commit-Queue: Ryan Tseng <hinoka@chromium.org>
changes/28/707728/4
Ryan Tseng 8 years ago committed by Commit Bot
parent 27db3f2c01
commit c3eb3fa335

@ -1308,10 +1308,6 @@ it or fix the checkout.
if cache_dir: if cache_dir:
cache_dir = os.path.join(self.root_dir, cache_dir) cache_dir = os.path.join(self.root_dir, cache_dir)
cache_dir = os.path.abspath(cache_dir) cache_dir = os.path.abspath(cache_dir)
# If running on a bot, force break any stale git cache locks.
if os.path.exists(cache_dir) and os.environ.get('CHROME_HEADLESS'):
subprocess2.check_call(['git', 'cache', 'unlock', '--cache-dir',
cache_dir, '--force', '--all'])
gclient_scm.GitWrapper.cache_dir = cache_dir gclient_scm.GitWrapper.cache_dir = cache_dir
git_cache.Mirror.SetCachePath(cache_dir) git_cache.Mirror.SetCachePath(cache_dir)
@ -2354,16 +2350,11 @@ def CMDsync(parser, args):
parser.add_option('--no_bootstrap', '--no-bootstrap', parser.add_option('--no_bootstrap', '--no-bootstrap',
action='store_true', action='store_true',
help='Don\'t bootstrap from Google Storage.') help='Don\'t bootstrap from Google Storage.')
parser.add_option('--ignore_locks', action='store_true',
help='GIT ONLY - Ignore cache locks.')
parser.add_option('--break_repo_locks', action='store_true', parser.add_option('--break_repo_locks', action='store_true',
help='GIT ONLY - Forcibly remove repo locks (e.g. ' help='GIT ONLY - Forcibly remove repo locks (e.g. '
'index.lock). This should only be used if you know for ' 'index.lock). This should only be used if you know for '
'certain that this invocation of gclient is the only ' 'certain that this invocation of gclient is the only '
'thing operating on the git repos (e.g. on a bot).') 'thing operating on the git repos (e.g. on a bot).')
parser.add_option('--lock_timeout', type='int', default=5000,
help='GIT ONLY - Deadline (in seconds) to wait for git '
'cache lock to become available. Default is %default.')
# TODO(agable): Remove these when the oldest CrOS release milestone is M56. # TODO(agable): Remove these when the oldest CrOS release milestone is M56.
parser.add_option('-t', '--transitive', action='store_true', parser.add_option('-t', '--transitive', action='store_true',
help='DEPRECATED: This is a no-op.') help='DEPRECATED: This is a no-op.')

@ -855,10 +855,7 @@ class GitWrapper(SCMWrapper):
depth = None depth = None
mirror.populate(verbose=options.verbose, mirror.populate(verbose=options.verbose,
bootstrap=not getattr(options, 'no_bootstrap', False), bootstrap=not getattr(options, 'no_bootstrap', False),
depth=depth, depth=depth)
ignore_lock=getattr(options, 'ignore_locks', False),
lock_timeout=getattr(options, 'lock_timeout', 0))
mirror.unlock()
def _Clone(self, revision, url, options): def _Clone(self, revision, url, options):
"""Clone a git repository from the given URL. """Clone a git repository from the given URL.

@ -35,9 +35,6 @@ except NameError:
class WinErr(Exception): class WinErr(Exception):
pass pass
class LockError(Exception):
pass
class ClobberNeeded(Exception): class ClobberNeeded(Exception):
pass pass
@ -77,122 +74,11 @@ def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
sleep_time *= 2 sleep_time *= 2
class Lockfile(object):
"""Class to represent a cross-platform process-specific lockfile."""
def __init__(self, path, timeout=0):
self.path = os.path.abspath(path)
self.timeout = timeout
self.lockfile = self.path + ".lock"
self.pid = os.getpid()
def _read_pid(self):
"""Read the pid stored in the lockfile.
Note: This method is potentially racy. By the time it returns the lockfile
may have been unlocked, removed, or stolen by some other process.
"""
try:
with open(self.lockfile, 'r') as f:
pid = int(f.readline().strip())
except (IOError, ValueError):
pid = None
return pid
def _make_lockfile(self):
"""Safely creates a lockfile containing the current pid."""
open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
fd = os.open(self.lockfile, open_flags, 0o644)
f = os.fdopen(fd, 'w')
print(self.pid, file=f)
f.close()
def _remove_lockfile(self):
"""Delete the lockfile. Complains (implicitly) if it doesn't exist.
See gclient_utils.py:rmtree docstring for more explanation on the
windows case.
"""
if sys.platform == 'win32':
lockfile = os.path.normcase(self.lockfile)
def delete():
exitcode = subprocess.call(['cmd.exe', '/c',
'del', '/f', '/q', lockfile])
if exitcode != 0:
raise LockError('Failed to remove lock: %s' % (lockfile,))
exponential_backoff_retry(
delete,
excs=(LockError,),
name='del [%s]' % (lockfile,))
else:
os.remove(self.lockfile)
def lock(self):
"""Acquire the lock.
This will block with a deadline of self.timeout seconds.
"""
elapsed = 0
while True:
try:
self._make_lockfile()
return
except OSError as e:
if elapsed < self.timeout:
sleep_time = max(10, min(3, self.timeout - elapsed))
logging.info('Could not create git cache lockfile; '
'will retry after sleep(%d).', sleep_time);
elapsed += sleep_time
time.sleep(sleep_time)
continue
if e.errno == errno.EEXIST:
raise LockError("%s is already locked" % self.path)
else:
raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
def unlock(self):
"""Release the lock."""
try:
if not self.is_locked():
raise LockError("%s is not locked" % self.path)
if not self.i_am_locking():
raise LockError("%s is locked, but not by me" % self.path)
self._remove_lockfile()
except WinErr:
# Windows is unreliable when it comes to file locking. YMMV.
pass
def break_lock(self):
"""Remove the lock, even if it was created by someone else."""
try:
self._remove_lockfile()
return True
except OSError as exc:
if exc.errno == errno.ENOENT:
return False
else:
raise
def is_locked(self):
"""Test if the file is locked by anyone.
Note: This method is potentially racy. By the time it returns the lockfile
may have been unlocked, removed, or stolen by some other process.
"""
return os.path.exists(self.lockfile)
def i_am_locking(self):
"""Test if the file is locked by this process."""
return self.is_locked() and self.pid == self._read_pid()
class Mirror(object): class Mirror(object):
git_exe = 'git.bat' if sys.platform.startswith('win') else 'git' git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
gsutil_exe = os.path.join( gsutil_exe = os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'gsutil.py') os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
cachepath_lock = threading.Lock()
@staticmethod @staticmethod
def parse_fetch_spec(spec): def parse_fetch_spec(spec):
@ -254,23 +140,21 @@ class Mirror(object):
@classmethod @classmethod
def SetCachePath(cls, cachepath): def SetCachePath(cls, cachepath):
with cls.cachepath_lock: setattr(cls, 'cachepath', cachepath)
setattr(cls, 'cachepath', cachepath)
@classmethod @classmethod
def GetCachePath(cls): def GetCachePath(cls):
with cls.cachepath_lock: if not hasattr(cls, 'cachepath'):
if not hasattr(cls, 'cachepath'): try:
try: cachepath = subprocess.check_output(
cachepath = subprocess.check_output( [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
[cls.git_exe, 'config', '--global', 'cache.cachepath']).strip() except subprocess.CalledProcessError:
except subprocess.CalledProcessError: cachepath = None
cachepath = None if not cachepath:
if not cachepath: raise RuntimeError(
raise RuntimeError( 'No global cache.cachepath git configuration found.')
'No global cache.cachepath git configuration found.') setattr(cls, 'cachepath', cachepath)
setattr(cls, 'cachepath', cachepath) return getattr(cls, 'cachepath')
return getattr(cls, 'cachepath')
def Rename(self, src, dst): def Rename(self, src, dst):
# This is somehow racy on Windows. # This is somehow racy on Windows.
@ -487,17 +371,12 @@ class Mirror(object):
raise ClobberNeeded() # Corrupted cache. raise ClobberNeeded() # Corrupted cache.
logging.warn('Fetch of %s failed' % spec) logging.warn('Fetch of %s failed' % spec)
def populate(self, depth=None, shallow=False, bootstrap=False, def populate(self, depth=None, shallow=False, bootstrap=False, verbose=False):
verbose=False, ignore_lock=False, lock_timeout=0):
assert self.GetCachePath() assert self.GetCachePath()
if shallow and not depth: if shallow and not depth:
depth = 10000 depth = 10000
gclient_utils.safe_makedirs(self.GetCachePath()) gclient_utils.safe_makedirs(self.GetCachePath())
lockfile = Lockfile(self.mirror_path, lock_timeout)
if not ignore_lock:
lockfile.lock()
tempdir = None tempdir = None
try: try:
tempdir = self._ensure_bootstrapped(depth, bootstrap) tempdir = self._ensure_bootstrapped(depth, bootstrap)
@ -515,8 +394,6 @@ class Mirror(object):
if os.path.exists(self.mirror_path): if os.path.exists(self.mirror_path):
gclient_utils.rmtree(self.mirror_path) gclient_utils.rmtree(self.mirror_path)
self.Rename(tempdir, self.mirror_path) self.Rename(tempdir, self.mirror_path)
if not ignore_lock:
lockfile.unlock()
def update_bootstrap(self, prune=False): def update_bootstrap(self, prune=False):
# The files are named <git number>.zip # The files are named <git number>.zip
@ -557,45 +434,6 @@ class Mirror(object):
except OSError: except OSError:
logging.warn('Unable to delete temporary pack file %s' % f) logging.warn('Unable to delete temporary pack file %s' % f)
@classmethod
def BreakLocks(cls, path):
did_unlock = False
lf = Lockfile(path)
if lf.break_lock():
did_unlock = True
# Look for lock files that might have been left behind by an interrupted
# git process.
lf = os.path.join(path, 'config.lock')
if os.path.exists(lf):
os.remove(lf)
did_unlock = True
cls.DeleteTmpPackFiles(path)
return did_unlock
def unlock(self):
return self.BreakLocks(self.mirror_path)
@classmethod
def UnlockAll(cls):
cachepath = cls.GetCachePath()
if not cachepath:
return
dirlist = os.listdir(cachepath)
repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
if os.path.isdir(os.path.join(cachepath, path))])
for dirent in dirlist:
if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
elif (dirent.endswith('.lock') and
os.path.isfile(os.path.join(cachepath, dirent))):
repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
unlocked_repos = []
for repo_dir in repo_dirs:
if cls.BreakLocks(repo_dir):
unlocked_repos.append(repo_dir)
return unlocked_repos
@subcommand.usage('[url of repo to check for caching]') @subcommand.usage('[url of repo to check for caching]')
def CMDexists(parser, args): def CMDexists(parser, args):
@ -647,9 +485,6 @@ def CMDpopulate(parser, args):
parser.add_option('--no_bootstrap', '--no-bootstrap', parser.add_option('--no_bootstrap', '--no-bootstrap',
action='store_true', action='store_true',
help='Don\'t bootstrap from Google Storage') help='Don\'t bootstrap from Google Storage')
parser.add_option('--ignore_locks', '--ignore-locks',
action='store_true',
help='Don\'t try to lock repository')
options, args = parser.parse_args(args) options, args = parser.parse_args(args)
if not len(args) == 1: if not len(args) == 1:
@ -661,8 +496,6 @@ def CMDpopulate(parser, args):
'verbose': options.verbose, 'verbose': options.verbose,
'shallow': options.shallow, 'shallow': options.shallow,
'bootstrap': not options.no_bootstrap, 'bootstrap': not options.no_bootstrap,
'ignore_lock': options.ignore_locks,
'lock_timeout': options.timeout,
} }
if options.depth: if options.depth:
kwargs['depth'] = options.depth kwargs['depth'] = options.depth
@ -707,7 +540,7 @@ def CMDfetch(parser, args):
if git_dir.startswith(cachepath): if git_dir.startswith(cachepath):
mirror = Mirror.FromPath(git_dir) mirror = Mirror.FromPath(git_dir)
mirror.populate( mirror.populate(
bootstrap=not options.no_bootstrap, lock_timeout=options.timeout) bootstrap=not options.no_bootstrap,)
return 0 return 0
for remote in remotes: for remote in remotes:
remote_url = subprocess.check_output( remote_url = subprocess.check_output(
@ -717,44 +550,11 @@ def CMDfetch(parser, args):
mirror.print = lambda *args: None mirror.print = lambda *args: None
print('Updating git cache...') print('Updating git cache...')
mirror.populate( mirror.populate(
bootstrap=not options.no_bootstrap, lock_timeout=options.timeout) bootstrap=not options.no_bootstrap)
subprocess.check_call([Mirror.git_exe, 'fetch', remote]) subprocess.check_call([Mirror.git_exe, 'fetch', remote])
return 0 return 0
@subcommand.usage('[url of repo to unlock, or -a|--all]')
def CMDunlock(parser, args):
"""Unlock one or all repos if their lock files are still around."""
parser.add_option('--force', '-f', action='store_true',
help='Actually perform the action')
parser.add_option('--all', '-a', action='store_true',
help='Unlock all repository caches')
options, args = parser.parse_args(args)
if len(args) > 1 or (len(args) == 0 and not options.all):
parser.error('git cache unlock takes exactly one repo url, or --all')
if not options.force:
cachepath = Mirror.GetCachePath()
lockfiles = [os.path.join(cachepath, path)
for path in os.listdir(cachepath)
if path.endswith('.lock') and os.path.isfile(path)]
parser.error('git cache unlock requires -f|--force to do anything. '
'Refusing to unlock the following repo caches: '
', '.join(lockfiles))
unlocked_repos = []
if options.all:
unlocked_repos.extend(Mirror.UnlockAll())
else:
m = Mirror(args[0])
if m.unlock():
unlocked_repos.append(m.mirror_path)
if unlocked_repos:
logging.info('Broke locks on these caches:\n %s' % '\n '.join(
unlocked_repos))
class OptionParser(optparse.OptionParser): class OptionParser(optparse.OptionParser):
"""Wrapper class for OptionParser to handle global options.""" """Wrapper class for OptionParser to handle global options."""
@ -766,8 +566,6 @@ class OptionParser(optparse.OptionParser):
help='Increase verbosity (can be passed multiple times)') help='Increase verbosity (can be passed multiple times)')
self.add_option('-q', '--quiet', action='store_true', self.add_option('-q', '--quiet', action='store_true',
help='Suppress all extraneous output') help='Suppress all extraneous output')
self.add_option('--timeout', type='int', default=0,
help='Timeout for acquiring cache lock, in seconds')
def parse_args(self, args=None, values=None): def parse_args(self, args=None, values=None):
options, args = optparse.OptionParser.parse_args(self, args, values) options, args = optparse.OptionParser.parse_args(self, args, values)

@ -331,7 +331,7 @@ def gclient_sync(
os.close(fd) os.close(fd)
args = ['sync', '--verbose', '--reset', '--force', args = ['sync', '--verbose', '--reset', '--force',
'--ignore_locks', '--output-json', gclient_output_file, '--output-json', gclient_output_file,
'--nohooks', '--noprehooks', '--delete_unversioned_trees'] '--nohooks', '--noprehooks', '--delete_unversioned_trees']
if with_branch_heads: if with_branch_heads:
args += ['--with_branch_heads'] args += ['--with_branch_heads']
@ -470,36 +470,9 @@ def is_broken_repo_dir(repo_dir):
return not path.exists(os.path.join(repo_dir, '.git', 'config')) return not path.exists(os.path.join(repo_dir, '.git', 'config'))
def _maybe_break_locks(checkout_path):
"""This removes all .lock files from this repo's .git directory.
In particular, this will cleanup index.lock files, as well as ref lock
files.
"""
git_dir = os.path.join(checkout_path, '.git')
for dirpath, _, filenames in os.walk(git_dir):
for filename in filenames:
if filename.endswith('.lock'):
to_break = os.path.join(dirpath, filename)
print 'breaking lock: %s' % to_break
try:
os.remove(to_break)
except OSError as ex:
print 'FAILED to break lock: %s: %s' % (to_break, ex)
raise
def git_checkout(solutions, revisions, shallow, refs, git_cache_dir, def git_checkout(solutions, revisions, shallow, refs, git_cache_dir,
cleanup_dir): cleanup_dir):
build_dir = os.getcwd() build_dir = os.getcwd()
# Before we do anything, break all git_cache locks.
if path.isdir(git_cache_dir):
git('cache', 'unlock', '-vv', '--force', '--all',
'--cache-dir', git_cache_dir)
for item in os.listdir(git_cache_dir):
filename = os.path.join(git_cache_dir, item)
if item.endswith('.lock'):
raise Exception('%s exists after cache unlock' % filename)
first_solution = True first_solution = True
for sln in solutions: for sln in solutions:
# Just in case we're hitting a different git server than the one from # Just in case we're hitting a different git server than the one from
@ -516,7 +489,7 @@ def git_checkout(solutions, revisions, shallow, refs, git_cache_dir,
shallow = False shallow = False
sln_dir = path.join(build_dir, name) sln_dir = path.join(build_dir, name)
s = ['--shallow'] if shallow else [] s = ['--shallow'] if shallow else []
populate_cmd = (['cache', 'populate', '--ignore_locks', '-v', populate_cmd = (['cache', 'populate', '-v',
'--cache-dir', git_cache_dir] + s + [url]) '--cache-dir', git_cache_dir] + s + [url])
for ref in refs: for ref in refs:
populate_cmd.extend(['--ref', ref]) populate_cmd.extend(['--ref', ref])
@ -544,18 +517,6 @@ def git_checkout(solutions, revisions, shallow, refs, git_cache_dir,
refspec = '%s:%s' % (ref, ref.lstrip('+')) refspec = '%s:%s' % (ref, ref.lstrip('+'))
git('fetch', 'origin', refspec, cwd=sln_dir) git('fetch', 'origin', refspec, cwd=sln_dir)
# Windows sometimes has trouble deleting files.
# This can make git commands that rely on locks fail.
# Try a few times in case Windows has trouble again (and again).
if sys.platform.startswith('win'):
tries = 3
while tries:
try:
_maybe_break_locks(sln_dir)
break
except Exception:
tries -= 1
revision = get_target_revision(name, url, revisions) or 'HEAD' revision = get_target_revision(name, url, revisions) or 'HEAD'
force_revision(sln_dir, revision) force_revision(sln_dir, revision)
done = True done = True

@ -231,20 +231,6 @@ class BotUpdateUnittests(unittest.TestCase):
gclient_sync_cmd = args gclient_sync_cmd = args
self.assertTrue('--break_repo_locks' in gclient_sync_cmd) self.assertTrue('--break_repo_locks' in gclient_sync_cmd)
def testGitCheckoutBreaksLocks(self):
self.overrideSetupForWindows()
path = '/b/build/slave/foo/build/.git'
lockfile = 'index.lock'
removed = []
old_os_walk = os.walk
old_os_remove = os.remove
setattr(os, 'walk', lambda _: [(path, None, [lockfile])])
setattr(os, 'remove', removed.append)
bot_update.ensure_checkout(**self.params)
setattr(os, 'walk', old_os_walk)
setattr(os, 'remove', old_os_remove)
self.assertTrue(os.path.join(path, lockfile) in removed)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

Loading…
Cancel
Save