changeset 1877:d314a89fa4f1

change lock format to let us detect and break stale locks. old style: symlink to pid new style: symlink to hostname:pid if lock code finds new-style lock, it breaks lock if locking pid is on same machine and pid is not alive. otherwise, lock is left alone. this makes locking code safe with old-style locks and with locks on other machines. new code makes server part of mercurial more robust in case machine crashes, power fails, or crazy user does kill -9.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Fri, 10 Mar 2006 08:31:31 -0800
parents 2e0fd78587bd
children a5c46cff620f
files mercurial/lock.py mercurial/util.py
diffstat 2 files changed, 75 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/lock.py
+++ b/mercurial/lock.py
@@ -6,7 +6,7 @@
 # of the GNU General Public License, incorporated herein by reference.
 
 from demandload import *
-demandload(globals(), 'errno os time util')
+demandload(globals(), 'errno os socket time util')
 
 class LockException(Exception):
     pass
@@ -16,11 +16,22 @@ class LockUnavailable(LockException):
     pass
 
 class lock(object):
+    # 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
+
     def __init__(self, file, timeout=-1, releasefn=None):
         self.f = file
         self.held = 0
         self.timeout = timeout
         self.releasefn = releasefn
+        self.id = None
+        self.host = None
+        self.pid = None
         self.lock()
 
     def __del__(self):
@@ -41,15 +52,50 @@ class lock(object):
                 raise inst
 
     def trylock(self):
-        pid = os.getpid()
+        if self.id is None:
+            self.host = socket.gethostname()
+            self.pid = os.getpid()
+            self.id = '%s:%s' % (self.host, self.pid)
+        while not self.held:
+            try:
+                util.makelock(self.id, self.f)
+                self.held = 1
+            except (OSError, IOError), why:
+                if why.errno == errno.EEXIST:
+                    locker = self.testlock()
+                    if locker:
+                        raise LockHeld(locker)
+                else:
+                    raise LockUnavailable(why)
+
+    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.
+        locker = util.readlock(self.f)
+        c = locker.find(':')
+        if c == -1:
+            return locker
+        host = locker[:c]
+        if host != self.host:
+            return locker
         try:
-            util.makelock(str(pid), self.f)
-            self.held = 1
-        except (OSError, IOError), why:
-            if why.errno == errno.EEXIST:
-                raise LockHeld(util.readlock(self.f))
-            else:
-                raise LockUnavailable(why)
+            pid = int(locker[c+1:])
+        except:
+            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:
+            l = lock(self.f + '.break')
+            l.trylock()
+            os.unlink(self.f)
+            l.release()
+        except (LockHeld, LockUnavailable):
+            return locker
 
     def release(self):
         if self.held:
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -499,7 +499,7 @@ if os.name == 'nt':
         return pf
 
     try: # ActivePython can create hard links using win32file module
-        import win32file
+        import win32api, win32con, win32file
 
         def os_link(src, dst): # NB will only succeed on NTFS
             win32file.CreateHardLink(dst, src)
@@ -516,8 +516,18 @@ if os.name == 'nt':
             except:
                 return os.stat(pathname).st_nlink
 
+        def testpid(pid):
+            '''return False if pid is dead, True if running or not known'''
+            try:
+                win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION,
+                                     False, pid)
+            except:
+                return True
+
     except ImportError:
-        pass
+        def testpid(pid):
+            '''return False if pid dead, True if running or not known'''
+            return True
 
     def is_exec(f, last):
         return last
@@ -614,6 +624,14 @@ else:
             else:
                 raise
 
+    def testpid(pid):
+        '''return False if pid dead, True if running or not sure'''
+        try:
+            os.kill(pid, 0)
+            return True
+        except OSError, inst:
+            return inst.errno != errno.ESRCH
+
     def explain_exit(code):
         """return a 2-tuple (desc, code) describing a process's status"""
         if os.WIFEXITED(code):