##// END OF EJS Templates
lock: refactor in preparation for next commit...
Valentin Gatien-Baron -
r44107:cd822413 default
parent child Browse files
Show More
@@ -1,436 +1,441 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 10 import contextlib
11 11 import errno
12 12 import os
13 13 import signal
14 14 import socket
15 15 import time
16 16 import warnings
17 17
18 18 from .i18n import _
19 19 from .pycompat import getattr
20 20
21 21 from . import (
22 22 encoding,
23 23 error,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28 from .utils import procutil
29 29
30 30
31 31 def _getlockprefix():
32 32 """Return a string which is used to differentiate pid namespaces
33 33
34 34 It's useful to detect "dead" processes and remove stale locks with
35 35 confidence. Typically it's just hostname. On modern linux, we include an
36 36 extra Linux-specific pid namespace identifier.
37 37 """
38 38 result = encoding.strtolocal(socket.gethostname())
39 39 if pycompat.sysplatform.startswith(b'linux'):
40 40 try:
41 41 result += b'/%x' % os.stat(b'/proc/self/ns/pid').st_ino
42 42 except OSError as ex:
43 43 if ex.errno not in (errno.ENOENT, errno.EACCES, errno.ENOTDIR):
44 44 raise
45 45 return result
46 46
47 47
48 48 @contextlib.contextmanager
49 49 def _delayedinterrupt():
50 50 """Block signal interrupt while doing something critical
51 51
52 52 This makes sure that the code block wrapped by this context manager won't
53 53 be interrupted.
54 54
55 55 For Windows developers: It appears not possible to guard time.sleep()
56 56 from CTRL_C_EVENT, so please don't use time.sleep() to test if this is
57 57 working.
58 58 """
59 59 assertedsigs = []
60 60 blocked = False
61 61 orighandlers = {}
62 62
63 63 def raiseinterrupt(num):
64 64 if num == getattr(signal, 'SIGINT', None) or num == getattr(
65 65 signal, 'CTRL_C_EVENT', None
66 66 ):
67 67 raise KeyboardInterrupt
68 68 else:
69 69 raise error.SignalInterrupt
70 70
71 71 def catchterm(num, frame):
72 72 if blocked:
73 73 assertedsigs.append(num)
74 74 else:
75 75 raiseinterrupt(num)
76 76
77 77 try:
78 78 # save handlers first so they can be restored even if a setup is
79 79 # interrupted between signal.signal() and orighandlers[] =.
80 80 for name in [
81 81 b'CTRL_C_EVENT',
82 82 b'SIGINT',
83 83 b'SIGBREAK',
84 84 b'SIGHUP',
85 85 b'SIGTERM',
86 86 ]:
87 87 num = getattr(signal, name, None)
88 88 if num and num not in orighandlers:
89 89 orighandlers[num] = signal.getsignal(num)
90 90 try:
91 91 for num in orighandlers:
92 92 signal.signal(num, catchterm)
93 93 except ValueError:
94 94 pass # in a thread? no luck
95 95
96 96 blocked = True
97 97 yield
98 98 finally:
99 99 # no simple way to reliably restore all signal handlers because
100 100 # any loops, recursive function calls, except blocks, etc. can be
101 101 # interrupted. so instead, make catchterm() raise interrupt.
102 102 blocked = False
103 103 try:
104 104 for num, handler in orighandlers.items():
105 105 signal.signal(num, handler)
106 106 except ValueError:
107 107 pass # in a thread?
108 108
109 109 # re-raise interrupt exception if any, which may be shadowed by a new
110 110 # interrupt occurred while re-raising the first one
111 111 if assertedsigs:
112 112 raiseinterrupt(assertedsigs[0])
113 113
114 114
115 115 def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs):
116 116 """return an acquired lock or raise an a LockHeld exception
117 117
118 118 This function is responsible to issue warnings and or debug messages about
119 119 the held lock while trying to acquires it."""
120 120
121 121 def printwarning(printer, locker):
122 122 """issue the usual "waiting on lock" message through any channel"""
123 123 # show more details for new-style locks
124 124 if b':' in locker:
125 125 host, pid = locker.split(b":", 1)
126 126 msg = _(
127 127 b"waiting for lock on %s held by process %r on host %r\n"
128 128 ) % (
129 129 pycompat.bytestr(l.desc),
130 130 pycompat.bytestr(pid),
131 131 pycompat.bytestr(host),
132 132 )
133 133 else:
134 134 msg = _(b"waiting for lock on %s held by %r\n") % (
135 135 l.desc,
136 136 pycompat.bytestr(locker),
137 137 )
138 138 printer(msg)
139 139
140 140 l = lock(vfs, lockname, 0, *args, dolock=False, **kwargs)
141 141
142 142 debugidx = 0 if (warntimeout and timeout) else -1
143 143 warningidx = 0
144 144 if not timeout:
145 145 warningidx = -1
146 146 elif warntimeout:
147 147 warningidx = warntimeout
148 148
149 149 delay = 0
150 150 while True:
151 151 try:
152 152 l._trylock()
153 153 break
154 154 except error.LockHeld as inst:
155 155 if delay == debugidx:
156 156 printwarning(ui.debug, inst.locker)
157 157 if delay == warningidx:
158 158 printwarning(ui.warn, inst.locker)
159 159 if timeout <= delay:
160 160 raise error.LockHeld(
161 161 errno.ETIMEDOUT, inst.filename, l.desc, inst.locker
162 162 )
163 163 time.sleep(1)
164 164 delay += 1
165 165
166 166 l.delay = delay
167 167 if l.delay:
168 168 if 0 <= warningidx <= l.delay:
169 169 ui.warn(_(b"got lock after %d seconds\n") % l.delay)
170 170 else:
171 171 ui.debug(b"got lock after %d seconds\n" % l.delay)
172 172 if l.acquirefn:
173 173 l.acquirefn()
174 174 return l
175 175
176 176
177 177 class lock(object):
178 178 '''An advisory lock held by one process to control access to a set
179 179 of files. Non-cooperating processes or incorrectly written scripts
180 180 can ignore Mercurial's locking scheme and stomp all over the
181 181 repository, so don't do that.
182 182
183 183 Typically used via localrepository.lock() to lock the repository
184 184 store (.hg/store/) or localrepository.wlock() to lock everything
185 185 else under .hg/.'''
186 186
187 187 # lock is symlink on platforms that support it, file on others.
188 188
189 189 # symlink is used because create of directory entry and contents
190 190 # are atomic even over nfs.
191 191
192 192 # old-style lock: symlink to pid
193 193 # new-style lock: symlink to hostname:pid
194 194
195 195 _host = None
196 196
197 197 def __init__(
198 198 self,
199 199 vfs,
200 200 fname,
201 201 timeout=-1,
202 202 releasefn=None,
203 203 acquirefn=None,
204 204 desc=None,
205 205 inheritchecker=None,
206 206 parentlock=None,
207 207 signalsafe=True,
208 208 dolock=True,
209 209 ):
210 210 self.vfs = vfs
211 211 self.f = fname
212 212 self.held = 0
213 213 self.timeout = timeout
214 214 self.releasefn = releasefn
215 215 self.acquirefn = acquirefn
216 216 self.desc = desc
217 217 self._inheritchecker = inheritchecker
218 218 self.parentlock = parentlock
219 219 self._parentheld = False
220 220 self._inherited = False
221 221 if signalsafe:
222 222 self._maybedelayedinterrupt = _delayedinterrupt
223 223 else:
224 224 self._maybedelayedinterrupt = util.nullcontextmanager
225 225 self.postrelease = []
226 226 self.pid = self._getpid()
227 227 if dolock:
228 228 self.delay = self.lock()
229 229 if self.acquirefn:
230 230 self.acquirefn()
231 231
232 232 def __enter__(self):
233 233 return self
234 234
235 235 def __exit__(self, exc_type, exc_value, exc_tb):
236 236 self.release()
237 237
238 238 def __del__(self):
239 239 if self.held:
240 240 warnings.warn(
241 241 "use lock.release instead of del lock",
242 242 category=DeprecationWarning,
243 243 stacklevel=2,
244 244 )
245 245
246 246 # ensure the lock will be removed
247 247 # even if recursive locking did occur
248 248 self.held = 1
249 249
250 250 self.release()
251 251
252 252 def _getpid(self):
253 253 # wrapper around procutil.getpid() to make testing easier
254 254 return procutil.getpid()
255 255
256 256 def lock(self):
257 257 timeout = self.timeout
258 258 while True:
259 259 try:
260 260 self._trylock()
261 261 return self.timeout - timeout
262 262 except error.LockHeld as inst:
263 263 if timeout != 0:
264 264 time.sleep(1)
265 265 if timeout > 0:
266 266 timeout -= 1
267 267 continue
268 268 raise error.LockHeld(
269 269 errno.ETIMEDOUT, inst.filename, self.desc, inst.locker
270 270 )
271 271
272 272 def _trylock(self):
273 273 if self.held:
274 274 self.held += 1
275 275 return
276 276 if lock._host is None:
277 277 lock._host = _getlockprefix()
278 278 lockname = b'%s:%d' % (lock._host, self.pid)
279 279 retry = 5
280 280 while not self.held and retry:
281 281 retry -= 1
282 282 try:
283 283 with self._maybedelayedinterrupt():
284 284 self.vfs.makelock(lockname, self.f)
285 285 self.held = 1
286 286 except (OSError, IOError) as why:
287 287 if why.errno == errno.EEXIST:
288 288 locker = self._readlock()
289 289 if locker is None:
290 290 continue
291 291
292 292 # special case where a parent process holds the lock -- this
293 293 # is different from the pid being different because we do
294 294 # want the unlock and postrelease functions to be called,
295 295 # but the lockfile to not be removed.
296 296 if locker == self.parentlock:
297 297 self._parentheld = True
298 298 self.held = 1
299 299 return
300 300 locker = self._testlock(locker)
301 301 if locker is not None:
302 302 raise error.LockHeld(
303 303 errno.EAGAIN,
304 304 self.vfs.join(self.f),
305 305 self.desc,
306 306 locker,
307 307 )
308 308 else:
309 309 raise error.LockUnavailable(
310 310 why.errno, why.strerror, why.filename, self.desc
311 311 )
312 312
313 313 if not self.held:
314 314 # use empty locker to mean "busy for frequent lock/unlock
315 315 # by many processes"
316 316 raise error.LockHeld(
317 317 errno.EAGAIN, self.vfs.join(self.f), self.desc, b""
318 318 )
319 319
320 320 def _readlock(self):
321 321 """read lock and return its value
322 322
323 323 Returns None if no lock exists, pid for old-style locks, and host:pid
324 324 for new-style locks.
325 325 """
326 326 try:
327 327 return self.vfs.readlock(self.f)
328 328 except (OSError, IOError) as why:
329 329 if why.errno == errno.ENOENT:
330 330 return None
331 331 raise
332 332
333 def _testlock(self, locker):
333 def _lockshouldbebroken(self, locker):
334 334 if locker is None:
335 return None
335 return False
336 336 try:
337 337 host, pid = locker.split(b":", 1)
338 338 except ValueError:
339 return locker
339 return False
340 340 if host != lock._host:
341 return locker
341 return False
342 342 try:
343 343 pid = int(pid)
344 344 except ValueError:
345 return locker
345 return False
346 346 if procutil.testpid(pid):
347 return False
348 return True
349
350 def _testlock(self, locker):
351 if not self._lockshouldbebroken(locker):
347 352 return locker
353
348 354 # if locker dead, break lock. must do this with another lock
349 355 # held, or can race and break valid lock.
350 356 try:
351 l = lock(self.vfs, self.f + b'.break', timeout=0)
352 self.vfs.unlink(self.f)
353 l.release()
357 with lock(self.vfs, self.f + b'.break', timeout=0):
358 self.vfs.unlink(self.f)
354 359 except error.LockError:
355 360 return locker
356 361
357 362 def testlock(self):
358 363 """return id of locker if lock is valid, else None.
359 364
360 365 If old-style lock, we cannot tell what machine locker is on.
361 366 with new-style lock, if locker is on this machine, we can
362 367 see if locker is alive. If locker is on this machine but
363 368 not alive, we can safely break lock.
364 369
365 370 The lock file is only deleted when None is returned.
366 371
367 372 """
368 373 locker = self._readlock()
369 374 return self._testlock(locker)
370 375
371 376 @contextlib.contextmanager
372 377 def inherit(self):
373 378 """context for the lock to be inherited by a Mercurial subprocess.
374 379
375 380 Yields a string that will be recognized by the lock in the subprocess.
376 381 Communicating this string to the subprocess needs to be done separately
377 382 -- typically by an environment variable.
378 383 """
379 384 if not self.held:
380 385 raise error.LockInheritanceContractViolation(
381 386 b'inherit can only be called while lock is held'
382 387 )
383 388 if self._inherited:
384 389 raise error.LockInheritanceContractViolation(
385 390 b'inherit cannot be called while lock is already inherited'
386 391 )
387 392 if self._inheritchecker is not None:
388 393 self._inheritchecker()
389 394 if self.releasefn:
390 395 self.releasefn()
391 396 if self._parentheld:
392 397 lockname = self.parentlock
393 398 else:
394 399 lockname = b'%s:%d' % (lock._host, self.pid)
395 400 self._inherited = True
396 401 try:
397 402 yield lockname
398 403 finally:
399 404 if self.acquirefn:
400 405 self.acquirefn()
401 406 self._inherited = False
402 407
403 408 def release(self):
404 409 """release the lock and execute callback function if any
405 410
406 411 If the lock has been acquired multiple times, the actual release is
407 412 delayed to the last release call."""
408 413 if self.held > 1:
409 414 self.held -= 1
410 415 elif self.held == 1:
411 416 self.held = 0
412 417 if self._getpid() != self.pid:
413 418 # we forked, and are not the parent
414 419 return
415 420 try:
416 421 if self.releasefn:
417 422 self.releasefn()
418 423 finally:
419 424 if not self._parentheld:
420 425 try:
421 426 self.vfs.unlink(self.f)
422 427 except OSError:
423 428 pass
424 429 # The postrelease functions typically assume the lock is not held
425 430 # at all.
426 431 if not self._parentheld:
427 432 for callback in self.postrelease:
428 433 callback()
429 434 # Prevent double usage and help clear cycles.
430 435 self.postrelease = None
431 436
432 437
433 438 def release(*locks):
434 439 for lock in locks:
435 440 if lock is not None:
436 441 lock.release()
General Comments 0
You need to be logged in to leave comments. Login now