##// END OF EJS Templates
lock: recognize parent locks while acquiring...
Siddharth Agarwal -
r26387:e16f80f8 default
parent child Browse files
Show More
@@ -1,222 +1,231
1 1 # lock.py - simple advisory locking scheme for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 12 import socket
13 13 import time
14 14 import warnings
15 15
16 16 from . import (
17 17 error,
18 18 util,
19 19 )
20 20
21 21 class lock(object):
22 22 '''An advisory lock held by one process to control access to a set
23 23 of files. Non-cooperating processes or incorrectly written scripts
24 24 can ignore Mercurial's locking scheme and stomp all over the
25 25 repository, so don't do that.
26 26
27 27 Typically used via localrepository.lock() to lock the repository
28 28 store (.hg/store/) or localrepository.wlock() to lock everything
29 29 else under .hg/.'''
30 30
31 31 # lock is symlink on platforms that support it, file on others.
32 32
33 33 # symlink is used because create of directory entry and contents
34 34 # are atomic even over nfs.
35 35
36 36 # old-style lock: symlink to pid
37 37 # new-style lock: symlink to hostname:pid
38 38
39 39 _host = None
40 40
41 41 def __init__(self, vfs, file, timeout=-1, releasefn=None, acquirefn=None,
42 42 desc=None, parentlock=None):
43 43 self.vfs = vfs
44 44 self.f = file
45 45 self.held = 0
46 46 self.timeout = timeout
47 47 self.releasefn = releasefn
48 48 self.acquirefn = acquirefn
49 49 self.desc = desc
50 50 self.parentlock = parentlock
51 51 self._parentheld = False
52 52 self._inherited = False
53 53 self.postrelease = []
54 54 self.pid = self._getpid()
55 55 self.delay = self.lock()
56 56 if self.acquirefn:
57 57 self.acquirefn()
58 58
59 59 def __del__(self):
60 60 if self.held:
61 61 warnings.warn("use lock.release instead of del lock",
62 62 category=DeprecationWarning,
63 63 stacklevel=2)
64 64
65 65 # ensure the lock will be removed
66 66 # even if recursive locking did occur
67 67 self.held = 1
68 68
69 69 self.release()
70 70
71 71 def _getpid(self):
72 72 # wrapper around os.getpid() to make testing easier
73 73 return os.getpid()
74 74
75 75 def lock(self):
76 76 timeout = self.timeout
77 77 while True:
78 78 try:
79 79 self._trylock()
80 80 return self.timeout - timeout
81 81 except error.LockHeld as inst:
82 82 if timeout != 0:
83 83 time.sleep(1)
84 84 if timeout > 0:
85 85 timeout -= 1
86 86 continue
87 87 raise error.LockHeld(errno.ETIMEDOUT, inst.filename, self.desc,
88 88 inst.locker)
89 89
90 90 def _trylock(self):
91 91 if self.held:
92 92 self.held += 1
93 93 return
94 94 if lock._host is None:
95 95 lock._host = socket.gethostname()
96 96 lockname = '%s:%s' % (lock._host, self.pid)
97 97 retry = 5
98 98 while not self.held and retry:
99 99 retry -= 1
100 100 try:
101 101 self.vfs.makelock(lockname, self.f)
102 102 self.held = 1
103 103 except (OSError, IOError) as why:
104 104 if why.errno == errno.EEXIST:
105 locker = self.testlock()
105 locker = self._readlock()
106 # special case where a parent process holds the lock -- this
107 # is different from the pid being different because we do
108 # want the unlock and postrelease functions to be called,
109 # but the lockfile to not be removed.
110 if locker == self.parentlock:
111 self._parentheld = True
112 self.held = 1
113 return
114 locker = self._testlock(locker)
106 115 if locker is not None:
107 116 raise error.LockHeld(errno.EAGAIN,
108 117 self.vfs.join(self.f), self.desc,
109 118 locker)
110 119 else:
111 120 raise error.LockUnavailable(why.errno, why.strerror,
112 121 why.filename, self.desc)
113 122
114 123 def _readlock(self):
115 124 """read lock and return its value
116 125
117 126 Returns None if no lock exists, pid for old-style locks, and host:pid
118 127 for new-style locks.
119 128 """
120 129 try:
121 130 return self.vfs.readlock(self.f)
122 131 except (OSError, IOError) as why:
123 132 if why.errno == errno.ENOENT:
124 133 return None
125 134 raise
126 135
127 136 def _testlock(self, locker):
128 137 if locker is None:
129 138 return None
130 139 try:
131 140 host, pid = locker.split(":", 1)
132 141 except ValueError:
133 142 return locker
134 143 if host != lock._host:
135 144 return locker
136 145 try:
137 146 pid = int(pid)
138 147 except ValueError:
139 148 return locker
140 149 if util.testpid(pid):
141 150 return locker
142 151 # if locker dead, break lock. must do this with another lock
143 152 # held, or can race and break valid lock.
144 153 try:
145 154 l = lock(self.vfs, self.f + '.break', timeout=0)
146 155 self.vfs.unlink(self.f)
147 156 l.release()
148 157 except error.LockError:
149 158 return locker
150 159
151 160 def testlock(self):
152 161 """return id of locker if lock is valid, else None.
153 162
154 163 If old-style lock, we cannot tell what machine locker is on.
155 164 with new-style lock, if locker is on this machine, we can
156 165 see if locker is alive. If locker is on this machine but
157 166 not alive, we can safely break lock.
158 167
159 168 The lock file is only deleted when None is returned.
160 169
161 170 """
162 171 locker = self._readlock()
163 172 return self._testlock(locker)
164 173
165 174 def prepinherit(self):
166 175 """prepare for the lock to be inherited by a Mercurial subprocess
167 176
168 177 Returns a string that will be recognized by the lock in the
169 178 subprocess. Communicating this string to the subprocess needs to be done
170 179 separately -- typically by an environment variable.
171 180 """
172 181 if not self.held:
173 182 raise error.LockInheritanceContractViolation(
174 183 'prepinherit can only be called while lock is held')
175 184 if self._inherited:
176 185 raise error.LockInheritanceContractViolation(
177 186 'prepinherit cannot be called while lock is already inherited')
178 187 if self.releasefn:
179 188 self.releasefn()
180 189 if self._parentheld:
181 190 lockname = self.parentlock
182 191 else:
183 192 lockname = '%s:%s' % (lock._host, self.pid)
184 193 self._inherited = True
185 194 return lockname
186 195
187 196 def reacquire(self):
188 197 if not self._inherited:
189 198 raise error.LockInheritanceContractViolation(
190 199 'reacquire can only be called after prepinherit')
191 200 if self.acquirefn:
192 201 self.acquirefn()
193 202 self._inherited = False
194 203
195 204 def release(self):
196 205 """release the lock and execute callback function if any
197 206
198 207 If the lock has been acquired multiple times, the actual release is
199 208 delayed to the last release call."""
200 209 if self.held > 1:
201 210 self.held -= 1
202 211 elif self.held == 1:
203 212 self.held = 0
204 213 if self._getpid() != self.pid:
205 214 # we forked, and are not the parent
206 215 return
207 216 try:
208 217 if self.releasefn:
209 218 self.releasefn()
210 219 finally:
211 220 if not self._parentheld:
212 221 try:
213 222 self.vfs.unlink(self.f)
214 223 except OSError:
215 224 pass
216 225 for callback in self.postrelease:
217 226 callback()
218 227
219 228 def release(*locks):
220 229 for lock in locks:
221 230 if lock is not None:
222 231 lock.release()
@@ -1,155 +1,256
1 1 from __future__ import absolute_import
2 2
3 3 import copy
4 4 import os
5 5 import silenttestrunner
6 6 import tempfile
7 7 import types
8 8 import unittest
9 9
10 10 from mercurial import (
11 11 lock,
12 12 scmutil,
13 13 )
14 14
15 15 testlockname = 'testlock'
16 16
17 17 # work around http://bugs.python.org/issue1515
18 18 if types.MethodType not in copy._deepcopy_dispatch:
19 19 def _deepcopy_method(x, memo):
20 20 return type(x)(x.im_func, copy.deepcopy(x.im_self, memo), x.im_class)
21 21 copy._deepcopy_dispatch[types.MethodType] = _deepcopy_method
22 22
23 23 class lockwrapper(lock.lock):
24 24 def __init__(self, pidoffset, *args, **kwargs):
25 25 # lock.lock.__init__() calls lock(), so the pidoffset assignment needs
26 26 # to be earlier
27 27 self._pidoffset = pidoffset
28 28 super(lockwrapper, self).__init__(*args, **kwargs)
29 29 def _getpid(self):
30 30 return os.getpid() + self._pidoffset
31 31
32 32 class teststate(object):
33 33 def __init__(self, testcase, dir, pidoffset=0):
34 34 self._testcase = testcase
35 35 self._acquirecalled = False
36 36 self._releasecalled = False
37 37 self._postreleasecalled = False
38 38 self.vfs = scmutil.vfs(dir, audit=False)
39 39 self._pidoffset = pidoffset
40 40
41 41 def makelock(self, *args, **kwargs):
42 42 l = lockwrapper(self._pidoffset, self.vfs, testlockname,
43 43 releasefn=self.releasefn, acquirefn=self.acquirefn,
44 44 *args, **kwargs)
45 45 l.postrelease.append(self.postreleasefn)
46 46 return l
47 47
48 48 def acquirefn(self):
49 49 self._acquirecalled = True
50 50
51 51 def releasefn(self):
52 52 self._releasecalled = True
53 53
54 54 def postreleasefn(self):
55 55 self._postreleasecalled = True
56 56
57 57 def assertacquirecalled(self, called):
58 58 self._testcase.assertEqual(
59 59 self._acquirecalled, called,
60 60 'expected acquire to be %s but was actually %s' % (
61 61 self._tocalled(called),
62 62 self._tocalled(self._acquirecalled),
63 63 ))
64 64
65 65 def resetacquirefn(self):
66 66 self._acquirecalled = False
67 67
68 68 def assertreleasecalled(self, called):
69 69 self._testcase.assertEqual(
70 70 self._releasecalled, called,
71 71 'expected release to be %s but was actually %s' % (
72 72 self._tocalled(called),
73 73 self._tocalled(self._releasecalled),
74 74 ))
75 75
76 76 def assertpostreleasecalled(self, called):
77 77 self._testcase.assertEqual(
78 78 self._postreleasecalled, called,
79 79 'expected postrelease to be %s but was actually %s' % (
80 80 self._tocalled(called),
81 81 self._tocalled(self._postreleasecalled),
82 82 ))
83 83
84 84 def assertlockexists(self, exists):
85 85 actual = self.vfs.lexists(testlockname)
86 86 self._testcase.assertEqual(
87 87 actual, exists,
88 88 'expected lock to %s but actually did %s' % (
89 89 self._toexists(exists),
90 90 self._toexists(actual),
91 91 ))
92 92
93 93 def _tocalled(self, called):
94 94 if called:
95 95 return 'called'
96 96 else:
97 97 return 'not called'
98 98
99 99 def _toexists(self, exists):
100 100 if exists:
101 101 return 'exist'
102 102 else:
103 103 return 'not exist'
104 104
105 105 class testlock(unittest.TestCase):
106 106 def testlock(self):
107 107 state = teststate(self, tempfile.mkdtemp(dir=os.getcwd()))
108 108 lock = state.makelock()
109 109 state.assertacquirecalled(True)
110 110 lock.release()
111 111 state.assertreleasecalled(True)
112 112 state.assertpostreleasecalled(True)
113 113 state.assertlockexists(False)
114 114
115 115 def testrecursivelock(self):
116 116 state = teststate(self, tempfile.mkdtemp(dir=os.getcwd()))
117 117 lock = state.makelock()
118 118 state.assertacquirecalled(True)
119 119
120 120 state.resetacquirefn()
121 121 lock.lock()
122 122 # recursive lock should not call acquirefn again
123 123 state.assertacquirecalled(False)
124 124
125 125 lock.release() # brings lock refcount down from 2 to 1
126 126 state.assertreleasecalled(False)
127 127 state.assertpostreleasecalled(False)
128 128 state.assertlockexists(True)
129 129
130 130 lock.release() # releases the lock
131 131 state.assertreleasecalled(True)
132 132 state.assertpostreleasecalled(True)
133 133 state.assertlockexists(False)
134 134
135 135 def testlockfork(self):
136 136 state = teststate(self, tempfile.mkdtemp(dir=os.getcwd()))
137 137 lock = state.makelock()
138 138 state.assertacquirecalled(True)
139 139
140 140 # fake a fork
141 141 forklock = copy.deepcopy(lock)
142 142 forklock._pidoffset = 1
143 143 forklock.release()
144 144 state.assertreleasecalled(False)
145 145 state.assertpostreleasecalled(False)
146 146 state.assertlockexists(True)
147 147
148 148 # release the actual lock
149 149 lock.release()
150 150 state.assertreleasecalled(True)
151 151 state.assertpostreleasecalled(True)
152 152 state.assertlockexists(False)
153 153
154 def testinheritlock(self):
155 d = tempfile.mkdtemp(dir=os.getcwd())
156 parentstate = teststate(self, d)
157 parentlock = parentstate.makelock()
158 parentstate.assertacquirecalled(True)
159
160 # set up lock inheritance
161 lockname = parentlock.prepinherit()
162 parentstate.assertreleasecalled(True)
163 parentstate.assertpostreleasecalled(False)
164 parentstate.assertlockexists(True)
165
166 childstate = teststate(self, d, pidoffset=1)
167 childlock = childstate.makelock(parentlock=lockname)
168 childstate.assertacquirecalled(True)
169
170 # release the child lock -- the lock file should still exist on disk
171 childlock.release()
172 childstate.assertreleasecalled(True)
173 childstate.assertpostreleasecalled(True)
174 childstate.assertlockexists(True)
175
176 parentstate.resetacquirefn()
177 parentlock.reacquire()
178 parentstate.assertacquirecalled(True)
179
180 parentlock.release()
181 parentstate.assertreleasecalled(True)
182 parentstate.assertpostreleasecalled(True)
183 parentstate.assertlockexists(False)
184
185 def testmultilock(self):
186 d = tempfile.mkdtemp(dir=os.getcwd())
187 state0 = teststate(self, d)
188 lock0 = state0.makelock()
189 state0.assertacquirecalled(True)
190
191 lock0name = lock0.prepinherit()
192 state0.assertreleasecalled(True)
193 state0.assertpostreleasecalled(False)
194 state0.assertlockexists(True)
195
196 state1 = teststate(self, d, pidoffset=1)
197 lock1 = state1.makelock(parentlock=lock0name)
198 state1.assertacquirecalled(True)
199
200 # from within lock1, acquire another lock
201 lock1name = lock1.prepinherit()
202 # since the file on disk is lock0's this should have the same name
203 self.assertEqual(lock0name, lock1name)
204
205 state2 = teststate(self, d, pidoffset=2)
206 lock2 = state2.makelock(parentlock=lock1name)
207 state2.assertacquirecalled(True)
208
209 lock2.release()
210 state2.assertreleasecalled(True)
211 state2.assertpostreleasecalled(True)
212 state2.assertlockexists(True)
213
214 state1.resetacquirefn()
215 lock1.reacquire()
216 state1.assertacquirecalled(True)
217
218 lock1.release()
219 state1.assertreleasecalled(True)
220 state1.assertpostreleasecalled(True)
221 state1.assertlockexists(True)
222
223 lock0.reacquire()
224 lock0.release()
225
226 def testinheritlockfork(self):
227 d = tempfile.mkdtemp(dir=os.getcwd())
228 parentstate = teststate(self, d)
229 parentlock = parentstate.makelock()
230 parentstate.assertacquirecalled(True)
231
232 # set up lock inheritance
233 lockname = parentlock.prepinherit()
234 childstate = teststate(self, d, pidoffset=1)
235 childlock = childstate.makelock(parentlock=lockname)
236 childstate.assertacquirecalled(True)
237
238 # fork the child lock
239 forkchildlock = copy.deepcopy(childlock)
240 forkchildlock._pidoffset += 1
241 forkchildlock.release()
242 childstate.assertreleasecalled(False)
243 childstate.assertpostreleasecalled(False)
244 childstate.assertlockexists(True)
245
246 # release the child lock
247 childlock.release()
248 childstate.assertreleasecalled(True)
249 childstate.assertpostreleasecalled(True)
250 childstate.assertlockexists(True)
251
252 parentlock.reacquire()
253 parentlock.release()
254
154 255 if __name__ == '__main__':
155 256 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now