1
# Copyright (C) 2006, 2007 Canonical Ltd
1
# Copyright (C) 2006-2010 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
"""On-disk mutex protecting a resource
21
21
internal locks (such as flock etc) because they can be seen across all
22
22
transports, including http.
24
Objects can be read if there is only physical read access; therefore
24
Objects can be read if there is only physical read access; therefore
25
25
readers can never be required to create a lock, though they will
26
26
check whether a writer is using the lock. Writers can't detect
27
27
whether anyone else is reading from the resource as they write.
57
57
The desired characteristics are:
59
* Locks are not reentrant. (That is, a client that tries to take a
59
* Locks are not reentrant. (That is, a client that tries to take a
60
60
lock it already holds may deadlock or fail.)
61
61
* Stale locks can be guessed at by a heuristic
62
62
* Lost locks can be broken by any client
78
78
and deadlocks will likely occur if the locks are aliased.
80
80
In the future we may add a "freshen" method which can be called
81
by a lock holder to check that their lock has not been broken, and to
81
by a lock holder to check that their lock has not been broken, and to
82
82
update the timestamp within it.
108
from cStringIO import StringIO
110
109
from bzrlib import (
114
115
import bzrlib.config
116
from bzrlib.decorators import only_raises
115
117
from bzrlib.errors import (
116
118
DirectoryNotEmpty,
118
120
LockBreakMismatch,
127
131
from bzrlib.trace import mutter, note
128
from bzrlib.transport import Transport
129
from bzrlib.osutils import rand_chars, format_delta
130
from bzrlib.rio import read_stanza, Stanza
132
from bzrlib.osutils import format_delta, rand_chars, get_host_name
135
from bzrlib.lazy_import import lazy_import
136
lazy_import(globals(), """
137
from bzrlib import rio
134
140
# XXX: At the moment there is no consideration of thread safety on LockDir
135
141
# objects. This should perhaps be updated - e.g. if two threads try to take a
146
152
# files/dirs created.
149
_DEFAULT_TIMEOUT_SECONDS = 300
155
_DEFAULT_TIMEOUT_SECONDS = 30
150
156
_DEFAULT_POLL_SECONDS = 1.0
153
class LockDir(object):
154
"""Write-lock guarding access to data."""
159
class LockDir(lock.Lock):
160
"""Write-lock guarding access to data.
156
163
__INFO_NAME = '/info'
163
170
:param transport: Transport which will contain the lock
165
:param path: Path to the lock within the base directory of the
172
:param path: Path to the lock within the base directory of the
168
assert isinstance(transport, Transport), \
169
("not a transport: %r" % transport)
170
175
self.transport = transport
172
177
self._lock_held = False
189
194
def create(self, mode=None):
190
195
"""Create the on-disk lock.
192
This is typically only called when the object/directory containing the
197
This is typically only called when the object/directory containing the
193
198
directory is first created. The lock is not held when it's created.
195
if self.transport.is_readonly():
196
raise UnlockableTransport(self.transport)
197
200
self._trace("create lock directory")
198
self.transport.mkdir(self.path, mode=mode)
202
self.transport.mkdir(self.path, mode=mode)
203
except (TransportError, PathError), e:
204
raise LockFailed(self, e)
200
207
def _attempt_lock(self):
201
208
"""Make the pending directory and attempt to rename into place.
203
210
If the rename succeeds, we read back the info file to check that we
204
211
really got the lock.
215
222
self._trace("lock_write...")
216
223
start_time = time.time()
217
tmpname = self._create_pending_dir()
225
tmpname = self._create_pending_dir()
226
except (errors.TransportError, PathError), e:
227
self._trace("... failed to create pending dir, %s", e)
228
raise LockFailed(self, e)
219
230
self.transport.rename(tmpname, self._held_dir)
220
except (PathError, DirectoryNotEmpty, FileExists, ResourceBusy), e:
231
except (errors.TransportError, PathError, DirectoryNotEmpty,
232
FileExists, ResourceBusy), e:
221
233
self._trace("... contention, %s", e)
222
234
self._remove_pending_dir(tmpname)
223
235
raise LockContention(self)
231
243
# incorrect. It's possible some other servers or filesystems will
232
244
# have a similar bug allowing someone to think they got the lock
233
245
# when it's already held.
247
# See <https://bugs.launchpad.net/bzr/+bug/498378> for one case.
249
# Strictly the check is unnecessary and a waste of time for most
250
# people, but probably worth trapping if something is wrong.
234
251
info = self.peek()
235
252
self._trace("after locking, info=%r", info)
236
if info['nonce'] != self.nonce:
254
raise LockFailed(self, "lock was renamed into place, but "
256
if info.get('nonce') != self.nonce:
237
257
self._trace("rename succeeded, "
238
258
"but lock is still held by someone else")
239
259
raise LockContention(self)
245
265
def _remove_pending_dir(self, tmpname):
246
266
"""Remove the pending directory
248
This is called if we failed to rename into place, so that the pending
268
This is called if we failed to rename into place, so that the pending
249
269
dirs don't clutter up the lockdir.
251
271
self._trace("remove %s", tmpname)
284
305
self._fake_read_lock = False
286
307
if not self._lock_held:
287
raise LockNotHeld(self)
308
return lock.cant_unlock_not_held(self)
288
309
if self._locked_via_token:
289
310
self._locked_via_token = False
290
311
self._lock_held = False
313
old_nonce = self.nonce
292
314
# rename before deleting, because we can't atomically remove the
294
316
start_time = time.time()
314
336
self.transport.delete_tree(tmpname)
315
337
self._trace("... unlock succeeded after %dms",
316
338
(time.time() - start_time) * 1000)
339
result = lock.LockResult(self.transport.abspath(self.path),
341
for hook in self.hooks['lock_released']:
318
344
def break_lock(self):
319
345
"""Break a lock not held by this instance of LockDir.
321
347
This is a UI centric function: it uses the bzrlib.ui.ui_factory to
322
348
prompt for input if a lock is detected and there is any doubt about
323
349
it possibly being still active.
351
:returns: LockResult for the broken lock.
325
353
self._check_not_locked()
326
holder_info = self.peek()
355
holder_info = self.peek()
356
except LockCorrupt, e:
357
# The lock info is corrupt.
358
if bzrlib.ui.ui_factory.get_boolean("Break (corrupt %r)" % (self,)):
359
self.force_break_corrupt(e.file_data)
327
361
if holder_info is not None:
328
362
lock_info = '\n'.join(self._format_lock_info(holder_info))
329
if bzrlib.ui.ui_factory.get_boolean("Break %s" % lock_info):
330
self.force_break(holder_info)
363
if bzrlib.ui.ui_factory.confirm_action(
364
"Break %(lock_info)s", 'bzrlib.lockdir.break',
365
dict(lock_info=lock_info)):
366
result = self.force_break(holder_info)
367
bzrlib.ui.ui_factory.show_message(
368
"Broke lock %s" % result.lock_url)
332
370
def force_break(self, dead_holder_info):
333
371
"""Release a lock held by another process.
342
380
LockBreakMismatch is raised.
344
382
After the lock is broken it will not be held by any process.
345
It is possible that another process may sneak in and take the
383
It is possible that another process may sneak in and take the
346
384
lock before the breaking process acquires it.
386
:returns: LockResult for the broken lock.
348
388
if not isinstance(dead_holder_info, dict):
349
389
raise ValueError("dead_holder_info: %r" % dead_holder_info)
357
397
tmpname = '%s/broken.%s.tmp' % (self.path, rand_chars(20))
358
398
self.transport.rename(self._held_dir, tmpname)
359
399
# check that we actually broke the right lock, not someone else;
360
# there's a small race window between checking it and doing the
400
# there's a small race window between checking it and doing the
362
402
broken_info_path = tmpname + self.__INFO_NAME
363
403
broken_info = self._read_info_file(broken_info_path)
365
405
raise LockBreakMismatch(self, broken_info, dead_holder_info)
366
406
self.transport.delete(broken_info_path)
367
407
self.transport.rmdir(tmpname)
408
result = lock.LockResult(self.transport.abspath(self.path),
409
current_info.get('nonce'))
410
for hook in self.hooks['lock_broken']:
414
def force_break_corrupt(self, corrupt_info_lines):
415
"""Release a lock that has been corrupted.
417
This is very similar to force_break, it except it doesn't assume that
418
self.peek() can work.
420
:param corrupt_info_lines: the lines of the corrupted info file, used
421
to check that the lock hasn't changed between reading the (corrupt)
422
info file and calling force_break_corrupt.
424
# XXX: this copes with unparseable info files, but what about missing
425
# info files? Or missing lock dirs?
426
self._check_not_locked()
427
tmpname = '%s/broken.%s.tmp' % (self.path, rand_chars(20))
428
self.transport.rename(self._held_dir, tmpname)
429
# check that we actually broke the right lock, not someone else;
430
# there's a small race window between checking it and doing the
432
broken_info_path = tmpname + self.__INFO_NAME
433
f = self.transport.get(broken_info_path)
434
broken_lines = f.readlines()
435
if broken_lines != corrupt_info_lines:
436
raise LockBreakMismatch(self, broken_lines, corrupt_info_lines)
437
self.transport.delete(broken_info_path)
438
self.transport.rmdir(tmpname)
439
result = lock.LockResult(self.transport.abspath(self.path))
440
for hook in self.hooks['lock_broken']:
369
443
def _check_not_locked(self):
370
444
"""If the lock is held by this instance, raise an error."""
378
452
or if the lock has been affected by a bug.
380
454
If the lock is not thought to be held, raises LockNotHeld. If
381
the lock is thought to be held but has been broken, raises
455
the lock is thought to be held but has been broken, raises
384
458
if not self._lock_held:
390
464
if info.get('nonce') != self.nonce:
391
465
# there is a lock, but not ours
392
466
raise LockBroken(self)
394
468
def _read_info_file(self, path):
395
469
"""Read one given info file.
397
471
peek() reads the info file of the lock holder, if any.
399
return self._parse_info(self.transport.get(path))
473
return self._parse_info(self.transport.get_bytes(path))
402
476
"""Check if the lock is held by anyone.
404
If it is held, this returns the lock info structure as a rio Stanza,
478
If it is held, this returns the lock info structure as a dict
405
479
which contains some information about the current lock holder.
406
480
Otherwise returns None.
409
483
info = self._read_info_file(self._held_info_path)
410
484
self._trace("peek -> held")
411
assert isinstance(info, dict), \
412
"bad parse result %r" % info
414
486
except NoSuchFile, e:
415
487
self._trace("peek -> not held")
417
489
def _prepare_info(self):
418
490
"""Write information about a pending lock to a temporary file.
421
492
# XXX: is creating this here inefficient?
422
493
config = bzrlib.config.GlobalConfig()
424
user = config.user_email()
425
except errors.NoEmailInUsername:
426
495
user = config.username()
427
s = Stanza(hostname=socket.gethostname(),
496
except errors.NoWhoami:
497
user = osutils.getuser_unicode()
498
s = rio.Stanza(hostname=get_host_name(),
428
499
pid=str(os.getpid()),
429
500
start_time=str(int(time.time())),
430
501
nonce=self.nonce,
433
504
return s.to_string()
435
def _parse_info(self, info_file):
436
return read_stanza(info_file.readlines()).as_dict()
506
def _parse_info(self, info_bytes):
507
lines = osutils.split_lines(info_bytes)
509
stanza = rio.read_stanza(lines)
510
except ValueError, e:
511
mutter('Corrupt lock info file: %r', lines)
512
raise LockCorrupt("could not parse lock info file: " + str(e),
515
# see bug 185013; we fairly often end up with the info file being
516
# empty after an interruption; we could log a message here but
517
# there may not be much we can say
520
return stanza.as_dict()
438
522
def attempt_lock(self):
439
523
"""Take the lock; fail if it's already held.
441
525
If you wish to block until the lock can be obtained, call wait_lock()
447
531
if self._fake_read_lock:
448
532
raise LockContention(self)
449
if self.transport.is_readonly():
450
raise UnlockableTransport(self.transport)
451
return self._attempt_lock()
533
result = self._attempt_lock()
534
hook_result = lock.LockResult(self.transport.abspath(self.path),
536
for hook in self.hooks['lock_acquired']:
453
540
def wait_lock(self, timeout=None, poll=None, max_attempts=None):
454
541
"""Wait a certain period for a lock.
503
590
if deadline_str is None:
504
591
deadline_str = time.strftime('%H:%M:%S',
505
592
time.localtime(deadline))
506
self._report_function('%s %s\n'
508
'%s\n' # locked ... ago
509
'Will continue to try until %s\n',
593
# As local lock urls are correct we display them.
594
# We avoid displaying remote lock urls.
595
lock_url = self.transport.abspath(self.path)
596
if lock_url.startswith('file://'):
597
lock_url = lock_url.split('.bzr/')[0]
600
user, hostname, pid, time_ago = formatted_info
601
msg = ('%s lock %s ' # lock_url
605
'[process #%s], ' # pid
606
'acquired %s.') # time ago
607
msg_args = [start, lock_url, user, hostname, pid, time_ago]
609
msg += ('\nWill continue to try until %s, unless '
611
msg_args.append(deadline_str)
612
msg += '\nSee "bzr help break-lock" for more.'
613
self._report_function(msg, *msg_args)
516
614
if (max_attempts is not None) and (attempt_count >= max_attempts):
517
615
self._trace("exceeded %d attempts")
518
616
raise LockContention(self)
520
618
self._trace("waiting %ss", poll)
621
# As timeout is always 0 for remote locks
622
# this block is applicable only for local
523
624
self._trace("timeout after waiting %ss", timeout)
524
raise LockContention(self)
625
raise LockContention('(local)', lock_url)
526
627
def leave_in_place(self):
527
628
self._locked_via_token = True
532
633
def lock_write(self, token=None):
533
634
"""Wait for and acquire the lock.
535
636
:param token: if this is already locked, then lock_write will fail
536
637
unless the token matches the existing lock.
537
638
:returns: a token if this instance supports tokens, otherwise None.
558
659
def lock_read(self):
559
660
"""Compatibility-mode shared lock.
561
LockDir doesn't support shared read-only locks, so this
662
LockDir doesn't support shared read-only locks, so this
562
663
just pretends that the lock is taken but really does nothing.
564
# At the moment Branches are commonly locked for read, but
665
# At the moment Branches are commonly locked for read, but
565
666
# we can't rely on that remotely. Once this is cleaned up,
566
# reenable this warning to prevent it coming back in
667
# reenable this warning to prevent it coming back in
567
668
# -- mbp 20060303
568
669
## warn("LockDir.lock_read falls back to write lock")
569
670
if self._lock_held or self._fake_read_lock:
573
674
def _format_lock_info(self, info):
574
675
"""Turn the contents of peek() into something for the user"""
575
lock_url = self.transport.abspath(self.path)
576
delta = time.time() - int(info['start_time'])
676
start_time = info.get('start_time')
677
if start_time is None:
678
time_ago = '(unknown)'
680
time_ago = format_delta(time.time() - int(info['start_time']))
681
user = info.get('user', '<unknown>')
682
hostname = info.get('hostname', '<unknown>')
683
pid = info.get('pid', '<unknown>')
578
'lock %s' % (lock_url,),
579
'held by %(user)s on host %(hostname)s [process #%(pid)s]' % info,
580
'locked %s' % (format_delta(delta),),
583
691
def validate_token(self, token):