mirror of
https://chromium.googlesource.com/chromium/tools/depot_tools.git
synced 2026-01-11 10:41:31 +00:00
After crrev.com/c/6486489 landed, gclient sync failed randomly due to lock failures. After investigation, it was concluded that the locks are not immediately released after the lock file handler close so that it fails to acquire the lock if gclient_scm.py attempts to lock it immediately after mirror.populate(). In Linux, there is no guarantee that the file close will release all the locks before processing the file description closure, unlike the CloseHandle() do in Microsoft Windows. This CL is to update the logic so that it releases the lock before os.close(). Bug: 407795715 Change-Id: I0f58627d368922f27c0590dcea2e7fde4242ae17 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/6497038 Auto-Submit: Scott Lee <ddoman@chromium.org> Commit-Queue: Josip Sokcevic <sokcevic@chromium.org> Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
132 lines
4.0 KiB
Python
132 lines
4.0 KiB
Python
# Copyright 2020 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Exclusive filelocking for all supported platforms."""
|
|
|
|
import contextlib
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
|
|
class LockError(Exception):
|
|
pass
|
|
|
|
|
|
if sys.platform.startswith('win'):
|
|
# Windows implementation
|
|
import win32imports
|
|
|
|
BYTES_TO_LOCK = 1
|
|
|
|
def _open_file(lockfile):
|
|
return win32imports.Handle(
|
|
win32imports.CreateFileW(
|
|
lockfile, # lpFileName
|
|
win32imports.GENERIC_WRITE, # dwDesiredAccess
|
|
0, # dwShareMode=prevent others from opening file
|
|
None, # lpSecurityAttributes
|
|
win32imports.CREATE_ALWAYS, # dwCreationDisposition
|
|
win32imports.FILE_ATTRIBUTE_NORMAL, # dwFlagsAndAttributes
|
|
None # hTemplateFile
|
|
))
|
|
|
|
def _close_file(handle, unlock):
|
|
if unlock:
|
|
# Locks are released *before* the CloseHandle function is finished
|
|
# processing:
|
|
# - https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-unlockfileex#remarks
|
|
pass
|
|
|
|
win32imports.CloseHandle(handle)
|
|
|
|
def _lock_file(handle):
|
|
ret = win32imports.LockFileEx(
|
|
handle, # hFile
|
|
win32imports.LOCKFILE_FAIL_IMMEDIATELY
|
|
| win32imports.LOCKFILE_EXCLUSIVE_LOCK, # dwFlags
|
|
0, #dwReserved
|
|
BYTES_TO_LOCK, # nNumberOfBytesToLockLow
|
|
0, # nNumberOfBytesToLockHigh
|
|
win32imports.Overlapped() # lpOverlapped
|
|
)
|
|
# LockFileEx returns result as bool, which is converted into an integer
|
|
# (1 == successful; 0 == not successful)
|
|
if ret == 0:
|
|
error_code = win32imports.GetLastError()
|
|
raise OSError('Failed to lock handle (error code: %d).' %
|
|
error_code)
|
|
else:
|
|
# Unix implementation
|
|
import fcntl
|
|
|
|
def _open_file(lockfile):
|
|
open_flags = (os.O_CREAT | os.O_WRONLY)
|
|
return os.open(lockfile, open_flags, 0o644)
|
|
|
|
def _close_file(fd, unlock):
|
|
# "man 2 fcntl" states that closing any file descriptor referring to
|
|
# the lock file will release all the process locks on the file, but
|
|
# there is no guarantee that the locks will be released atomically
|
|
# before the closure.
|
|
#
|
|
# It's necessary to release the lock before the file close to avoid
|
|
# possible race conditions.
|
|
if unlock:
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
|
|
def _lock_file(fd):
|
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
|
|
|
|
def _try_lock(lockfile):
|
|
f = _open_file(lockfile)
|
|
try:
|
|
_lock_file(f)
|
|
except Exception:
|
|
_close_file(f, unlock=False)
|
|
raise
|
|
return lambda: _close_file(f, unlock=True)
|
|
|
|
|
|
def _lock(path, timeout=0):
|
|
"""_lock returns function to release the lock if locking was successful.
|
|
|
|
_lock also implements simple retry logic.
|
|
NOTE: timeout value doesn't include time it takes to aquire lock, just
|
|
overall sleep time."""
|
|
elapsed = 0
|
|
sleep_time = 0.1
|
|
while True:
|
|
try:
|
|
return _try_lock(path + '.locked')
|
|
except (OSError, IOError) as e:
|
|
if elapsed < timeout:
|
|
logging.info(
|
|
'Could not create git cache lockfile; '
|
|
'will retry after sleep(%d).', sleep_time)
|
|
elapsed += sleep_time
|
|
time.sleep(sleep_time)
|
|
continue
|
|
raise LockError("Error locking %s (err: %s)" % (path, str(e)))
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def lock(path, timeout=0):
|
|
"""Get exclusive lock to path.
|
|
|
|
Usage:
|
|
import lockfile
|
|
with lockfile.lock(path, timeout):
|
|
# Do something
|
|
pass
|
|
|
|
"""
|
|
release_fn = _lock(path, timeout)
|
|
try:
|
|
yield
|
|
finally:
|
|
release_fn()
|