##// 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 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, inheritchecker=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._inheritchecker = inheritchecker
52 52 self.parentlock = parentlock
53 53 self._parentheld = False
54 54 self._inherited = False
55 55 self.postrelease = []
56 56 self.pid = self._getpid()
57 57 self.delay = self.lock()
58 58 if self.acquirefn:
59 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 67 def __del__(self):
62 68 if self.held:
63 69 warnings.warn("use lock.release instead of del lock",
64 70 category=DeprecationWarning,
65 71 stacklevel=2)
66 72
67 73 # ensure the lock will be removed
68 74 # even if recursive locking did occur
69 75 self.held = 1
70 76
71 77 self.release()
72 78
73 79 def _getpid(self):
74 80 # wrapper around os.getpid() to make testing easier
75 81 return os.getpid()
76 82
77 83 def lock(self):
78 84 timeout = self.timeout
79 85 while True:
80 86 try:
81 87 self._trylock()
82 88 return self.timeout - timeout
83 89 except error.LockHeld as inst:
84 90 if timeout != 0:
85 91 time.sleep(1)
86 92 if timeout > 0:
87 93 timeout -= 1
88 94 continue
89 95 raise error.LockHeld(errno.ETIMEDOUT, inst.filename, self.desc,
90 96 inst.locker)
91 97
92 98 def _trylock(self):
93 99 if self.held:
94 100 self.held += 1
95 101 return
96 102 if lock._host is None:
97 103 lock._host = socket.gethostname()
98 104 lockname = '%s:%s' % (lock._host, self.pid)
99 105 retry = 5
100 106 while not self.held and retry:
101 107 retry -= 1
102 108 try:
103 109 self.vfs.makelock(lockname, self.f)
104 110 self.held = 1
105 111 except (OSError, IOError) as why:
106 112 if why.errno == errno.EEXIST:
107 113 locker = self._readlock()
108 114 # special case where a parent process holds the lock -- this
109 115 # is different from the pid being different because we do
110 116 # want the unlock and postrelease functions to be called,
111 117 # but the lockfile to not be removed.
112 118 if locker == self.parentlock:
113 119 self._parentheld = True
114 120 self.held = 1
115 121 return
116 122 locker = self._testlock(locker)
117 123 if locker is not None:
118 124 raise error.LockHeld(errno.EAGAIN,
119 125 self.vfs.join(self.f), self.desc,
120 126 locker)
121 127 else:
122 128 raise error.LockUnavailable(why.errno, why.strerror,
123 129 why.filename, self.desc)
124 130
125 131 def _readlock(self):
126 132 """read lock and return its value
127 133
128 134 Returns None if no lock exists, pid for old-style locks, and host:pid
129 135 for new-style locks.
130 136 """
131 137 try:
132 138 return self.vfs.readlock(self.f)
133 139 except (OSError, IOError) as why:
134 140 if why.errno == errno.ENOENT:
135 141 return None
136 142 raise
137 143
138 144 def _testlock(self, locker):
139 145 if locker is None:
140 146 return None
141 147 try:
142 148 host, pid = locker.split(":", 1)
143 149 except ValueError:
144 150 return locker
145 151 if host != lock._host:
146 152 return locker
147 153 try:
148 154 pid = int(pid)
149 155 except ValueError:
150 156 return locker
151 157 if util.testpid(pid):
152 158 return locker
153 159 # if locker dead, break lock. must do this with another lock
154 160 # held, or can race and break valid lock.
155 161 try:
156 162 l = lock(self.vfs, self.f + '.break', timeout=0)
157 163 self.vfs.unlink(self.f)
158 164 l.release()
159 165 except error.LockError:
160 166 return locker
161 167
162 168 def testlock(self):
163 169 """return id of locker if lock is valid, else None.
164 170
165 171 If old-style lock, we cannot tell what machine locker is on.
166 172 with new-style lock, if locker is on this machine, we can
167 173 see if locker is alive. If locker is on this machine but
168 174 not alive, we can safely break lock.
169 175
170 176 The lock file is only deleted when None is returned.
171 177
172 178 """
173 179 locker = self._readlock()
174 180 return self._testlock(locker)
175 181
176 182 @contextlib.contextmanager
177 183 def inherit(self):
178 184 """context for the lock to be inherited by a Mercurial subprocess.
179 185
180 186 Yields a string that will be recognized by the lock in the subprocess.
181 187 Communicating this string to the subprocess needs to be done separately
182 188 -- typically by an environment variable.
183 189 """
184 190 if not self.held:
185 191 raise error.LockInheritanceContractViolation(
186 192 'inherit can only be called while lock is held')
187 193 if self._inherited:
188 194 raise error.LockInheritanceContractViolation(
189 195 'inherit cannot be called while lock is already inherited')
190 196 if self._inheritchecker is not None:
191 197 self._inheritchecker()
192 198 if self.releasefn:
193 199 self.releasefn()
194 200 if self._parentheld:
195 201 lockname = self.parentlock
196 202 else:
197 203 lockname = '%s:%s' % (lock._host, self.pid)
198 204 self._inherited = True
199 205 try:
200 206 yield lockname
201 207 finally:
202 208 if self.acquirefn:
203 209 self.acquirefn()
204 210 self._inherited = False
205 211
206 212 def release(self):
207 213 """release the lock and execute callback function if any
208 214
209 215 If the lock has been acquired multiple times, the actual release is
210 216 delayed to the last release call."""
211 217 if self.held > 1:
212 218 self.held -= 1
213 219 elif self.held == 1:
214 220 self.held = 0
215 221 if self._getpid() != self.pid:
216 222 # we forked, and are not the parent
217 223 return
218 224 try:
219 225 if self.releasefn:
220 226 self.releasefn()
221 227 finally:
222 228 if not self._parentheld:
223 229 try:
224 230 self.vfs.unlink(self.f)
225 231 except OSError:
226 232 pass
227 233 # The postrelease functions typically assume the lock is not held
228 234 # at all.
229 235 if not self._parentheld:
230 236 for callback in self.postrelease:
231 237 callback()
232 238
233 239 def release(*locks):
234 240 for lock in locks:
235 241 if lock is not None:
236 242 lock.release()
General Comments 0
You need to be logged in to leave comments. Login now