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