##// END OF EJS Templates
lock: while releasing, unlink lockfile even if the release function throws...
Siddharth Agarwal -
r23032:f484be02 default
parent child Browse files
Show More
@@ -1,154 +1,156 b''
1 # lock.py - simple advisory locking scheme for mercurial
1 # lock.py - simple advisory locking scheme for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import util, error
8 import util, error
9 import errno, os, socket, time
9 import errno, os, socket, time
10 import warnings
10 import warnings
11
11
12 class lock(object):
12 class lock(object):
13 '''An advisory lock held by one process to control access to a set
13 '''An advisory lock held by one process to control access to a set
14 of files. Non-cooperating processes or incorrectly written scripts
14 of files. Non-cooperating processes or incorrectly written scripts
15 can ignore Mercurial's locking scheme and stomp all over the
15 can ignore Mercurial's locking scheme and stomp all over the
16 repository, so don't do that.
16 repository, so don't do that.
17
17
18 Typically used via localrepository.lock() to lock the repository
18 Typically used via localrepository.lock() to lock the repository
19 store (.hg/store/) or localrepository.wlock() to lock everything
19 store (.hg/store/) or localrepository.wlock() to lock everything
20 else under .hg/.'''
20 else under .hg/.'''
21
21
22 # lock is symlink on platforms that support it, file on others.
22 # lock is symlink on platforms that support it, file on others.
23
23
24 # symlink is used because create of directory entry and contents
24 # symlink is used because create of directory entry and contents
25 # are atomic even over nfs.
25 # are atomic even over nfs.
26
26
27 # old-style lock: symlink to pid
27 # old-style lock: symlink to pid
28 # new-style lock: symlink to hostname:pid
28 # new-style lock: symlink to hostname:pid
29
29
30 _host = None
30 _host = None
31
31
32 def __init__(self, vfs, file, timeout=-1, releasefn=None, desc=None):
32 def __init__(self, vfs, file, timeout=-1, releasefn=None, desc=None):
33 self.vfs = vfs
33 self.vfs = vfs
34 self.f = file
34 self.f = file
35 self.held = 0
35 self.held = 0
36 self.timeout = timeout
36 self.timeout = timeout
37 self.releasefn = releasefn
37 self.releasefn = releasefn
38 self.desc = desc
38 self.desc = desc
39 self.postrelease = []
39 self.postrelease = []
40 self.pid = os.getpid()
40 self.pid = os.getpid()
41 self.delay = self.lock()
41 self.delay = self.lock()
42
42
43 def __del__(self):
43 def __del__(self):
44 if self.held:
44 if self.held:
45 warnings.warn("use lock.release instead of del lock",
45 warnings.warn("use lock.release instead of del lock",
46 category=DeprecationWarning,
46 category=DeprecationWarning,
47 stacklevel=2)
47 stacklevel=2)
48
48
49 # ensure the lock will be removed
49 # ensure the lock will be removed
50 # even if recursive locking did occur
50 # even if recursive locking did occur
51 self.held = 1
51 self.held = 1
52
52
53 self.release()
53 self.release()
54
54
55 def lock(self):
55 def lock(self):
56 timeout = self.timeout
56 timeout = self.timeout
57 while True:
57 while True:
58 try:
58 try:
59 self.trylock()
59 self.trylock()
60 return self.timeout - timeout
60 return self.timeout - timeout
61 except error.LockHeld, inst:
61 except error.LockHeld, inst:
62 if timeout != 0:
62 if timeout != 0:
63 time.sleep(1)
63 time.sleep(1)
64 if timeout > 0:
64 if timeout > 0:
65 timeout -= 1
65 timeout -= 1
66 continue
66 continue
67 raise error.LockHeld(errno.ETIMEDOUT, inst.filename, self.desc,
67 raise error.LockHeld(errno.ETIMEDOUT, inst.filename, self.desc,
68 inst.locker)
68 inst.locker)
69
69
70 def trylock(self):
70 def trylock(self):
71 if self.held:
71 if self.held:
72 self.held += 1
72 self.held += 1
73 return
73 return
74 if lock._host is None:
74 if lock._host is None:
75 lock._host = socket.gethostname()
75 lock._host = socket.gethostname()
76 lockname = '%s:%s' % (lock._host, self.pid)
76 lockname = '%s:%s' % (lock._host, self.pid)
77 while not self.held:
77 while not self.held:
78 try:
78 try:
79 self.vfs.makelock(lockname, self.f)
79 self.vfs.makelock(lockname, self.f)
80 self.held = 1
80 self.held = 1
81 except (OSError, IOError), why:
81 except (OSError, IOError), why:
82 if why.errno == errno.EEXIST:
82 if why.errno == errno.EEXIST:
83 locker = self.testlock()
83 locker = self.testlock()
84 if locker is not None:
84 if locker is not None:
85 raise error.LockHeld(errno.EAGAIN,
85 raise error.LockHeld(errno.EAGAIN,
86 self.vfs.join(self.f), self.desc,
86 self.vfs.join(self.f), self.desc,
87 locker)
87 locker)
88 else:
88 else:
89 raise error.LockUnavailable(why.errno, why.strerror,
89 raise error.LockUnavailable(why.errno, why.strerror,
90 why.filename, self.desc)
90 why.filename, self.desc)
91
91
92 def testlock(self):
92 def testlock(self):
93 """return id of locker if lock is valid, else None.
93 """return id of locker if lock is valid, else None.
94
94
95 If old-style lock, we cannot tell what machine locker is on.
95 If old-style lock, we cannot tell what machine locker is on.
96 with new-style lock, if locker is on this machine, we can
96 with new-style lock, if locker is on this machine, we can
97 see if locker is alive. If locker is on this machine but
97 see if locker is alive. If locker is on this machine but
98 not alive, we can safely break lock.
98 not alive, we can safely break lock.
99
99
100 The lock file is only deleted when None is returned.
100 The lock file is only deleted when None is returned.
101
101
102 """
102 """
103 try:
103 try:
104 locker = self.vfs.readlock(self.f)
104 locker = self.vfs.readlock(self.f)
105 except (OSError, IOError), why:
105 except (OSError, IOError), why:
106 if why.errno == errno.ENOENT:
106 if why.errno == errno.ENOENT:
107 return None
107 return None
108 raise
108 raise
109 try:
109 try:
110 host, pid = locker.split(":", 1)
110 host, pid = locker.split(":", 1)
111 except ValueError:
111 except ValueError:
112 return locker
112 return locker
113 if host != lock._host:
113 if host != lock._host:
114 return locker
114 return locker
115 try:
115 try:
116 pid = int(pid)
116 pid = int(pid)
117 except ValueError:
117 except ValueError:
118 return locker
118 return locker
119 if util.testpid(pid):
119 if util.testpid(pid):
120 return locker
120 return locker
121 # if locker dead, break lock. must do this with another lock
121 # if locker dead, break lock. must do this with another lock
122 # held, or can race and break valid lock.
122 # held, or can race and break valid lock.
123 try:
123 try:
124 l = lock(self.vfs, self.f + '.break', timeout=0)
124 l = lock(self.vfs, self.f + '.break', timeout=0)
125 self.vfs.unlink(self.f)
125 self.vfs.unlink(self.f)
126 l.release()
126 l.release()
127 except error.LockError:
127 except error.LockError:
128 return locker
128 return locker
129
129
130 def release(self):
130 def release(self):
131 """release the lock and execute callback function if any
131 """release the lock and execute callback function if any
132
132
133 If the lock has been acquired multiple times, the actual release is
133 If the lock has been acquired multiple times, the actual release is
134 delayed to the last release call."""
134 delayed to the last release call."""
135 if self.held > 1:
135 if self.held > 1:
136 self.held -= 1
136 self.held -= 1
137 elif self.held == 1:
137 elif self.held == 1:
138 self.held = 0
138 self.held = 0
139 if os.getpid() != self.pid:
139 if os.getpid() != self.pid:
140 # we forked, and are not the parent
140 # we forked, and are not the parent
141 return
141 return
142 if self.releasefn:
143 self.releasefn()
144 try:
142 try:
145 self.vfs.unlink(self.f)
143 if self.releasefn:
146 except OSError:
144 self.releasefn()
147 pass
145 finally:
146 try:
147 self.vfs.unlink(self.f)
148 except OSError:
149 pass
148 for callback in self.postrelease:
150 for callback in self.postrelease:
149 callback()
151 callback()
150
152
151 def release(*locks):
153 def release(*locks):
152 for lock in locks:
154 for lock in locks:
153 if lock is not None:
155 if lock is not None:
154 lock.release()
156 lock.release()
@@ -1,41 +1,77 b''
1 #require unix-permissions no-root no-windows
1 #require unix-permissions no-root no-windows
2
2
3 Prepare
3 Prepare
4
4
5 $ hg init a
5 $ hg init a
6 $ echo a > a/a
6 $ echo a > a/a
7 $ hg -R a ci -A -m a
7 $ hg -R a ci -A -m a
8 adding a
8 adding a
9
9
10 $ hg clone a b
10 $ hg clone a b
11 updating to branch default
11 updating to branch default
12 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
12 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
13
13
14 Test that raising an exception in the release function doesn't cause the lock to choke
15
16 $ cat > testlock.py << EOF
17 > from mercurial import cmdutil, error, util
18 >
19 > cmdtable = {}
20 > command = cmdutil.command(cmdtable)
21 >
22 > def acquiretestlock(repo, releaseexc):
23 > def unlock():
24 > if releaseexc:
25 > raise util.Abort('expected release exception')
26 > l = repo._lock(repo.vfs, 'testlock', False, unlock, None, 'test lock')
27 > return l
28 >
29 > @command('testlockexc')
30 > def testlockexc(ui, repo):
31 > testlock = acquiretestlock(repo, True)
32 > try:
33 > testlock.release()
34 > finally:
35 > try:
36 > testlock = acquiretestlock(repo, False)
37 > except error.LockHeld:
38 > raise util.Abort('lockfile on disk even after releasing!')
39 > testlock.release()
40 > EOF
41 $ cat >> $HGRCPATH << EOF
42 > [extensions]
43 > testlock=$TESTTMP/testlock.py
44 > EOF
45
46 $ hg -R b testlockexc
47 abort: expected release exception
48 [255]
49
14 One process waiting for another
50 One process waiting for another
15
51
16 $ cat > hooks.py << EOF
52 $ cat > hooks.py << EOF
17 > import time
53 > import time
18 > def sleepone(**x): time.sleep(1)
54 > def sleepone(**x): time.sleep(1)
19 > def sleephalf(**x): time.sleep(0.5)
55 > def sleephalf(**x): time.sleep(0.5)
20 > EOF
56 > EOF
21 $ echo b > b/b
57 $ echo b > b/b
22 $ hg -R b ci -A -m b --config hooks.precommit="python:`pwd`/hooks.py:sleepone" > stdout &
58 $ hg -R b ci -A -m b --config hooks.precommit="python:`pwd`/hooks.py:sleepone" > stdout &
23 $ hg -R b up -q --config hooks.pre-update="python:`pwd`/hooks.py:sleephalf"
59 $ hg -R b up -q --config hooks.pre-update="python:`pwd`/hooks.py:sleephalf"
24 waiting for lock on working directory of b held by '*:*' (glob)
60 waiting for lock on working directory of b held by '*:*' (glob)
25 got lock after ? seconds (glob)
61 got lock after ? seconds (glob)
26 warning: ignoring unknown working parent d2ae7f538514!
62 warning: ignoring unknown working parent d2ae7f538514!
27 $ wait
63 $ wait
28 $ cat stdout
64 $ cat stdout
29 adding b
65 adding b
30
66
31 Pushing to a local read-only repo that can't be locked
67 Pushing to a local read-only repo that can't be locked
32
68
33 $ chmod 100 a/.hg/store
69 $ chmod 100 a/.hg/store
34
70
35 $ hg -R b push a
71 $ hg -R b push a
36 pushing to a
72 pushing to a
37 searching for changes
73 searching for changes
38 abort: could not lock repository a: Permission denied
74 abort: could not lock repository a: Permission denied
39 [255]
75 [255]
40
76
41 $ chmod 700 a/.hg/store
77 $ chmod 700 a/.hg/store
General Comments 0
You need to be logged in to leave comments. Login now