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