lock.py
329 lines
| 10.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / lock.py
|
r9309 | # lock.py - simple advisory locking scheme for mercurial | ||
|
r161 | # | ||
|
r2859 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||
|
r161 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r161 | |||
|
r25956 | from __future__ import absolute_import | ||
|
r26473 | import contextlib | ||
|
r25956 | import errno | ||
|
r30921 | import os | ||
|
r25956 | import socket | ||
import time | ||||
|
r8113 | import warnings | ||
|
r161 | |||
|
r35209 | from .i18n import _ | ||
|
r25956 | from . import ( | ||
|
r31375 | encoding, | ||
|
r25956 | error, | ||
|
r30921 | pycompat, | ||
|
r25956 | util, | ||
) | ||||
|
r30920 | def _getlockprefix(): | ||
"""Return a string which is used to differentiate pid namespaces | ||||
It's useful to detect "dead" processes and remove stale locks with | ||||
|
r30921 | confidence. Typically it's just hostname. On modern linux, we include an | ||
extra Linux-specific pid namespace identifier. | ||||
|
r30920 | """ | ||
|
r31378 | result = socket.gethostname() | ||
if pycompat.ispy3: | ||||
result = result.encode(pycompat.sysstr(encoding.encoding), 'replace') | ||||
|
r30921 | if pycompat.sysplatform.startswith('linux'): | ||
try: | ||||
result += '/%x' % os.stat('/proc/self/ns/pid').st_ino | ||||
except OSError as ex: | ||||
if ex.errno not in (errno.ENOENT, errno.EACCES, errno.ENOTDIR): | ||||
raise | ||||
return result | ||||
|
r30920 | |||
|
r35210 | def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs): | ||
|
r35209 | """return an acquired lock or raise an a LockHeld exception | ||
|
r35210 | This function is responsible to issue warnings and or debug messages about | ||
the held lock while trying to acquires it.""" | ||||
|
r35209 | |||
def printwarning(printer, locker): | ||||
"""issue the usual "waiting on lock" message through any channel""" | ||||
# show more details for new-style locks | ||||
if ':' in locker: | ||||
host, pid = locker.split(":", 1) | ||||
msg = _("waiting for lock on %s held by process %r " | ||||
"on host %r\n") % (l.desc, pid, host) | ||||
else: | ||||
msg = _("waiting for lock on %s held by %r\n") % (l.desc, locker) | ||||
printer(msg) | ||||
l = lock(vfs, lockname, 0, *args, dolock=False, **kwargs) | ||||
|
r35210 | debugidx = 0 if (warntimeout and timeout) else -1 | ||
|
r35209 | warningidx = 0 | ||
if not timeout: | ||||
warningidx = -1 | ||||
|
r35210 | elif warntimeout: | ||
warningidx = warntimeout | ||||
|
r35209 | |||
delay = 0 | ||||
while True: | ||||
try: | ||||
l._trylock() | ||||
break | ||||
except error.LockHeld as inst: | ||||
|
r35210 | if delay == debugidx: | ||
printwarning(ui.debug, inst.locker) | ||||
|
r35209 | if delay == warningidx: | ||
printwarning(ui.warn, inst.locker) | ||||
if timeout <= delay: | ||||
raise error.LockHeld(errno.ETIMEDOUT, inst.filename, | ||||
l.desc, inst.locker) | ||||
time.sleep(1) | ||||
delay += 1 | ||||
l.delay = delay | ||||
if l.delay: | ||||
|
r35210 | if 0 <= warningidx <= l.delay: | ||
ui.warn(_("got lock after %s seconds\n") % l.delay) | ||||
else: | ||||
ui.debug("got lock after %s seconds\n" % l.delay) | ||||
|
r35209 | if l.acquirefn: | ||
l.acquirefn() | ||||
return l | ||||
|
r1559 | class lock(object): | ||
|
r9309 | '''An advisory lock held by one process to control access to a set | ||
of files. Non-cooperating processes or incorrectly written scripts | ||||
can ignore Mercurial's locking scheme and stomp all over the | ||||
repository, so don't do that. | ||||
Typically used via localrepository.lock() to lock the repository | ||||
store (.hg/store/) or localrepository.wlock() to lock everything | ||||
else under .hg/.''' | ||||
|
r1877 | # lock is symlink on platforms that support it, file on others. | ||
# symlink is used because create of directory entry and contents | ||||
# are atomic even over nfs. | ||||
# old-style lock: symlink to pid | ||||
# new-style lock: symlink to hostname:pid | ||||
|
r4947 | _host = None | ||
|
r26321 | def __init__(self, vfs, file, timeout=-1, releasefn=None, acquirefn=None, | ||
|
r35209 | desc=None, inheritchecker=None, parentlock=None, | ||
dolock=True): | ||||
|
r20091 | self.vfs = vfs | ||
|
r161 | self.f = file | ||
self.held = 0 | ||||
|
r1787 | self.timeout = timeout | ||
|
r1530 | self.releasefn = releasefn | ||
|
r26321 | self.acquirefn = acquirefn | ||
|
r2016 | self.desc = desc | ||
|
r26498 | self._inheritchecker = inheritchecker | ||
|
r26356 | self.parentlock = parentlock | ||
self._parentheld = False | ||||
self._inherited = False | ||||
|
r15589 | self.postrelease = [] | ||
|
r26383 | self.pid = self._getpid() | ||
|
r35209 | if dolock: | ||
self.delay = self.lock() | ||||
if self.acquirefn: | ||||
self.acquirefn() | ||||
|
r161 | |||
|
r27797 | def __enter__(self): | ||
return self | ||||
def __exit__(self, exc_type, exc_value, exc_tb): | ||||
self.release() | ||||
|
r161 | def __del__(self): | ||
|
r8108 | if self.held: | ||
|
r8113 | warnings.warn("use lock.release instead of del lock", | ||
category=DeprecationWarning, | ||||
stacklevel=2) | ||||
|
r8108 | # ensure the lock will be removed | ||
# even if recursive locking did occur | ||||
self.held = 1 | ||||
|
r161 | self.release() | ||
|
r26383 | def _getpid(self): | ||
|
r28027 | # wrapper around util.getpid() to make testing easier | ||
return util.getpid() | ||||
|
r26383 | |||
|
r161 | def lock(self): | ||
|
r1787 | timeout = self.timeout | ||
|
r14494 | while True: | ||
|
r161 | try: | ||
|
r26082 | self._trylock() | ||
|
r20380 | return self.timeout - timeout | ||
|
r25660 | except error.LockHeld as inst: | ||
|
r1787 | if timeout != 0: | ||
|
r161 | time.sleep(1) | ||
|
r1787 | if timeout > 0: | ||
timeout -= 1 | ||||
|
r161 | continue | ||
|
r7640 | raise error.LockHeld(errno.ETIMEDOUT, inst.filename, self.desc, | ||
inst.locker) | ||||
|
r515 | |||
|
r26082 | def _trylock(self): | ||
|
r8108 | if self.held: | ||
self.held += 1 | ||||
return | ||||
|
r4947 | if lock._host is None: | ||
|
r30920 | lock._host = _getlockprefix() | ||
|
r31354 | lockname = '%s:%d' % (lock._host, self.pid) | ||
|
r26081 | retry = 5 | ||
while not self.held and retry: | ||||
retry -= 1 | ||||
|
r1877 | try: | ||
|
r20091 | self.vfs.makelock(lockname, self.f) | ||
|
r1877 | self.held = 1 | ||
|
r25660 | except (OSError, IOError) as why: | ||
|
r1877 | if why.errno == errno.EEXIST: | ||
|
r26387 | locker = self._readlock() | ||
|
r32088 | if locker is None: | ||
continue | ||||
|
r26387 | # special case where a parent process holds the lock -- this | ||
# is different from the pid being different because we do | ||||
# want the unlock and postrelease functions to be called, | ||||
# but the lockfile to not be removed. | ||||
if locker == self.parentlock: | ||||
self._parentheld = True | ||||
self.held = 1 | ||||
return | ||||
locker = self._testlock(locker) | ||||
|
r3686 | if locker is not None: | ||
|
r20091 | raise error.LockHeld(errno.EAGAIN, | ||
self.vfs.join(self.f), self.desc, | ||||
|
r7640 | locker) | ||
|
r1877 | else: | ||
|
r7640 | raise error.LockUnavailable(why.errno, why.strerror, | ||
why.filename, self.desc) | ||||
|
r1877 | |||
|
r32089 | if not self.held: | ||
# use empty locker to mean "busy for frequent lock/unlock | ||||
# by many processes" | ||||
raise error.LockHeld(errno.EAGAIN, | ||||
self.vfs.join(self.f), self.desc, "") | ||||
|
r26290 | def _readlock(self): | ||
"""read lock and return its value | ||||
Returns None if no lock exists, pid for old-style locks, and host:pid | ||||
for new-style locks. | ||||
""" | ||||
try: | ||||
return self.vfs.readlock(self.f) | ||||
except (OSError, IOError) as why: | ||||
if why.errno == errno.ENOENT: | ||||
return None | ||||
raise | ||||
|
r26291 | def _testlock(self, locker): | ||
|
r26290 | if locker is None: | ||
return None | ||||
|
r2579 | try: | ||
host, pid = locker.split(":", 1) | ||||
except ValueError: | ||||
|
r1877 | return locker | ||
|
r4947 | if host != lock._host: | ||
|
r1877 | return locker | ||
|
r161 | try: | ||
|
r2579 | pid = int(pid) | ||
|
r9685 | except ValueError: | ||
|
r1877 | return locker | ||
if util.testpid(pid): | ||||
return locker | ||||
# if locker dead, break lock. must do this with another lock | ||||
# held, or can race and break valid lock. | ||||
try: | ||||
|
r20091 | l = lock(self.vfs, self.f + '.break', timeout=0) | ||
self.vfs.unlink(self.f) | ||||
|
r1877 | l.release() | ||
|
r7640 | except error.LockError: | ||
|
r1877 | return locker | ||
|
r161 | |||
|
r26291 | def testlock(self): | ||
"""return id of locker if lock is valid, else None. | ||||
If old-style lock, we cannot tell what machine locker is on. | ||||
with new-style lock, if locker is on this machine, we can | ||||
see if locker is alive. If locker is on this machine but | ||||
not alive, we can safely break lock. | ||||
The lock file is only deleted when None is returned. | ||||
""" | ||||
locker = self._readlock() | ||||
return self._testlock(locker) | ||||
|
r26473 | @contextlib.contextmanager | ||
def inherit(self): | ||||
"""context for the lock to be inherited by a Mercurial subprocess. | ||||
|
r26357 | |||
|
r26473 | Yields a string that will be recognized by the lock in the subprocess. | ||
Communicating this string to the subprocess needs to be done separately | ||||
-- typically by an environment variable. | ||||
|
r26357 | """ | ||
if not self.held: | ||||
raise error.LockInheritanceContractViolation( | ||||
|
r26473 | 'inherit can only be called while lock is held') | ||
|
r26357 | if self._inherited: | ||
raise error.LockInheritanceContractViolation( | ||||
|
r26473 | 'inherit cannot be called while lock is already inherited') | ||
|
r26498 | if self._inheritchecker is not None: | ||
self._inheritchecker() | ||||
|
r26357 | if self.releasefn: | ||
self.releasefn() | ||||
if self._parentheld: | ||||
lockname = self.parentlock | ||||
else: | ||||
lockname = '%s:%s' % (lock._host, self.pid) | ||||
self._inherited = True | ||||
|
r26473 | try: | ||
yield lockname | ||||
finally: | ||||
if self.acquirefn: | ||||
self.acquirefn() | ||||
self._inherited = False | ||||
|
r26358 | |||
|
r161 | def release(self): | ||
|
r15583 | """release the lock and execute callback function if any | ||
|
r17537 | If the lock has been acquired multiple times, the actual release is | ||
|
r17510 | delayed to the last release call.""" | ||
|
r8108 | if self.held > 1: | ||
self.held -= 1 | ||||
|
r9680 | elif self.held == 1: | ||
|
r161 | self.held = 0 | ||
|
r26383 | if self._getpid() != self.pid: | ||
|
r18907 | # we forked, and are not the parent | ||
return | ||||
|
r503 | try: | ||
|
r23032 | if self.releasefn: | ||
self.releasefn() | ||||
finally: | ||||
|
r26359 | if not self._parentheld: | ||
try: | ||||
self.vfs.unlink(self.f) | ||||
except OSError: | ||||
pass | ||||
|
r26474 | # The postrelease functions typically assume the lock is not held | ||
# at all. | ||||
if not self._parentheld: | ||||
for callback in self.postrelease: | ||||
callback() | ||||
|
r28959 | # Prevent double usage and help clear cycles. | ||
self.postrelease = None | ||||
|
r161 | |||
|
r8108 | def release(*locks): | ||
for lock in locks: | ||||
if lock is not None: | ||||
lock.release() | ||||