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