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