~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/lockdir.py

  • Committer: Andrew Bennetts
  • Date: 2007-03-26 06:24:01 UTC
  • mto: This revision was merged to the branch mainline in revision 2376.
  • Revision ID: andrew.bennetts@canonical.com-20070326062401-k3nbefzje5332jaf
Deal with review comments from Robert:

  * Add my name to the NEWS file
  * Move the test case to a new module in branch_implementations
  * Remove revision_history cruft from identitymap and test_identitymap
  * Improve some docstrings

Also, this fixes a bug where revision_history was not returning a copy of the
cached data, allowing the cache to be corrupted.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2006 Canonical Ltd
2
2
#
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
88
88
>>> t = MemoryTransport()
89
89
>>> l = LockDir(t, 'sample-lock')
90
90
>>> l.create()
91
 
>>> token = l.wait_lock()
 
91
>>> l.wait_lock()
92
92
>>> # do something here
93
93
>>> l.unlock()
94
94
 
95
95
"""
96
96
 
97
 
 
98
 
# TODO: We sometimes have the problem that our attempt to rename '1234' to
99
 
# 'held' fails because the transport server moves into an existing directory,
100
 
# rather than failing the rename.  If we made the info file name the same as
101
 
# the locked directory name we would avoid this problem because moving into
102
 
# the held directory would implicitly clash.  However this would not mesh with
103
 
# the existing locking code and needs a new format of the containing object.
104
 
# -- robertc, mbp 20070628
105
 
 
106
97
import os
107
98
import time
108
99
from cStringIO import StringIO
109
100
 
110
101
from bzrlib import (
111
 
    debug,
112
102
    errors,
113
103
    )
114
104
import bzrlib.config
118
108
        LockBreakMismatch,
119
109
        LockBroken,
120
110
        LockContention,
121
 
        LockFailed,
122
111
        LockNotHeld,
123
112
        NoSuchFile,
124
113
        PathError,
125
114
        ResourceBusy,
126
 
        TransportError,
127
115
        UnlockableTransport,
128
116
        )
129
117
from bzrlib.trace import mutter, note
167
155
        :param path: Path to the lock within the base directory of the 
168
156
            transport.
169
157
        """
 
158
        assert isinstance(transport, Transport), \
 
159
            ("not a transport: %r" % transport)
170
160
        self.transport = transport
171
161
        self.path = path
172
162
        self._lock_held = False
173
 
        self._locked_via_token = False
174
163
        self._fake_read_lock = False
175
164
        self._held_dir = path + '/held'
176
165
        self._held_info_path = self._held_dir + self.__INFO_NAME
177
166
        self._file_modebits = file_modebits
178
167
        self._dir_modebits = dir_modebits
 
168
        self.nonce = rand_chars(20)
179
169
 
180
170
        self._report_function = note
181
171
 
192
182
        This is typically only called when the object/directory containing the 
193
183
        directory is first created.  The lock is not held when it's created.
194
184
        """
195
 
        self._trace("create lock directory")
196
 
        try:
197
 
            self.transport.mkdir(self.path, mode=mode)
198
 
        except (TransportError, PathError), e:
199
 
            raise LockFailed(self, e)
200
 
 
201
 
 
202
 
    def _attempt_lock(self):
203
 
        """Make the pending directory and attempt to rename into place.
 
185
        if self.transport.is_readonly():
 
186
            raise UnlockableTransport(self.transport)
 
187
        self.transport.mkdir(self.path, mode=mode)
 
188
 
 
189
    def attempt_lock(self):
 
190
        """Take the lock; fail if it's already held.
204
191
        
205
 
        If the rename succeeds, we read back the info file to check that we
206
 
        really got the lock.
207
 
 
208
 
        If we fail to acquire the lock, this method is responsible for
209
 
        cleaning up the pending directory if possible.  (But it doesn't do
210
 
        that yet.)
211
 
 
212
 
        :returns: The nonce of the lock, if it was successfully acquired.
213
 
 
214
 
        :raises LockContention: If the lock is held by someone else.  The exception
215
 
            contains the info of the current holder of the lock.
 
192
        If you wish to block until the lock can be obtained, call wait_lock()
 
193
        instead.
216
194
        """
217
 
        self._trace("lock_write...")
218
 
        start_time = time.time()
219
 
        try:
220
 
            tmpname = self._create_pending_dir()
221
 
        except (errors.TransportError, PathError), e:
222
 
            self._trace("... failed to create pending dir, %s", e)
223
 
            raise LockFailed(self, e)
224
 
        try:
 
195
        if self._fake_read_lock:
 
196
            raise LockContention(self)
 
197
        if self.transport.is_readonly():
 
198
            raise UnlockableTransport(self.transport)
 
199
        try:
 
200
            tmpname = '%s/pending.%s.tmp' % (self.path, rand_chars(20))
 
201
            try:
 
202
                self.transport.mkdir(tmpname)
 
203
            except NoSuchFile:
 
204
                # This may raise a FileExists exception
 
205
                # which is okay, it will be caught later and determined
 
206
                # to be a LockContention.
 
207
                self.create(mode=self._dir_modebits)
 
208
                
 
209
                # After creating the lock directory, try again
 
210
                self.transport.mkdir(tmpname)
 
211
 
 
212
            info_bytes = self._prepare_info()
 
213
            # We use put_file_non_atomic because we just created a new unique
 
214
            # directory so we don't have to worry about files existing there.
 
215
            # We'll rename the whole directory into place to get atomic
 
216
            # properties
 
217
            self.transport.put_bytes_non_atomic(tmpname + self.__INFO_NAME,
 
218
                                                info_bytes)
 
219
 
225
220
            self.transport.rename(tmpname, self._held_dir)
226
 
        except (errors.TransportError, PathError, DirectoryNotEmpty,
227
 
                FileExists, ResourceBusy), e:
228
 
            self._trace("... contention, %s", e)
229
 
            self._remove_pending_dir(tmpname)
230
 
            raise LockContention(self)
231
 
        except Exception, e:
232
 
            self._trace("... lock failed, %s", e)
233
 
            self._remove_pending_dir(tmpname)
 
221
            self._lock_held = True
 
222
            self.confirm()
 
223
        except errors.PermissionDenied:
234
224
            raise
235
 
        # We must check we really got the lock, because Launchpad's sftp
236
 
        # server at one time had a bug were the rename would successfully
237
 
        # move the new directory into the existing directory, which was
238
 
        # incorrect.  It's possible some other servers or filesystems will
239
 
        # have a similar bug allowing someone to think they got the lock
240
 
        # when it's already held.
241
 
        info = self.peek()
242
 
        self._trace("after locking, info=%r", info)
243
 
        if info['nonce'] != self.nonce:
244
 
            self._trace("rename succeeded, "
245
 
                "but lock is still held by someone else")
 
225
        except (PathError, DirectoryNotEmpty, FileExists, ResourceBusy), e:
 
226
            mutter("contention on %r: %s", self, e)
246
227
            raise LockContention(self)
247
 
        self._lock_held = True
248
 
        self._trace("... lock succeeded after %dms",
249
 
                (time.time() - start_time) * 1000)
250
 
        return self.nonce
251
 
 
252
 
    def _remove_pending_dir(self, tmpname):
253
 
        """Remove the pending directory
254
 
 
255
 
        This is called if we failed to rename into place, so that the pending 
256
 
        dirs don't clutter up the lockdir.
257
 
        """
258
 
        self._trace("remove %s", tmpname)
259
 
        try:
260
 
            self.transport.delete(tmpname + self.__INFO_NAME)
261
 
            self.transport.rmdir(tmpname)
262
 
        except PathError, e:
263
 
            note("error removing pending lock: %s", e)
264
 
 
265
 
    def _create_pending_dir(self):
266
 
        tmpname = '%s/%s.tmp' % (self.path, rand_chars(10))
267
 
        try:
268
 
            self.transport.mkdir(tmpname)
269
 
        except NoSuchFile:
270
 
            # This may raise a FileExists exception
271
 
            # which is okay, it will be caught later and determined
272
 
            # to be a LockContention.
273
 
            self._trace("lock directory does not exist, creating it")
274
 
            self.create(mode=self._dir_modebits)
275
 
            # After creating the lock directory, try again
276
 
            self.transport.mkdir(tmpname)
277
 
        self.nonce = rand_chars(20)
278
 
        info_bytes = self._prepare_info()
279
 
        # We use put_file_non_atomic because we just created a new unique
280
 
        # directory so we don't have to worry about files existing there.
281
 
        # We'll rename the whole directory into place to get atomic
282
 
        # properties
283
 
        self.transport.put_bytes_non_atomic(tmpname + self.__INFO_NAME,
284
 
                                            info_bytes)
285
 
        return tmpname
286
228
 
287
229
    def unlock(self):
288
230
        """Release a held lock
292
234
            return
293
235
        if not self._lock_held:
294
236
            raise LockNotHeld(self)
295
 
        if self._locked_via_token:
296
 
            self._locked_via_token = False
297
 
            self._lock_held = False
298
 
        else:
299
 
            # rename before deleting, because we can't atomically remove the
300
 
            # whole tree
301
 
            start_time = time.time()
302
 
            self._trace("unlocking")
303
 
            tmpname = '%s/releasing.%s.tmp' % (self.path, rand_chars(20))
304
 
            # gotta own it to unlock
305
 
            self.confirm()
306
 
            self.transport.rename(self._held_dir, tmpname)
307
 
            self._lock_held = False
308
 
            self.transport.delete(tmpname + self.__INFO_NAME)
309
 
            try:
310
 
                self.transport.rmdir(tmpname)
311
 
            except DirectoryNotEmpty, e:
312
 
                # There might have been junk left over by a rename that moved
313
 
                # another locker within the 'held' directory.  do a slower
314
 
                # deletion where we list the directory and remove everything
315
 
                # within it.
316
 
                #
317
 
                # Maybe this should be broader to allow for ftp servers with
318
 
                # non-specific error messages?
319
 
                self._trace("doing recursive deletion of non-empty directory "
320
 
                        "%s", tmpname)
321
 
                self.transport.delete_tree(tmpname)
322
 
            self._trace("... unlock succeeded after %dms",
323
 
                    (time.time() - start_time) * 1000)
 
237
        # rename before deleting, because we can't atomically remove the whole
 
238
        # tree
 
239
        tmpname = '%s/releasing.%s.tmp' % (self.path, rand_chars(20))
 
240
        # gotta own it to unlock
 
241
        self.confirm()
 
242
        self.transport.rename(self._held_dir, tmpname)
 
243
        self._lock_held = False
 
244
        self.transport.delete(tmpname + self.__INFO_NAME)
 
245
        self.transport.rmdir(tmpname)
324
246
 
325
247
    def break_lock(self):
326
248
        """Break a lock not held by this instance of LockDir.
414
336
        """
415
337
        try:
416
338
            info = self._read_info_file(self._held_info_path)
417
 
            self._trace("peek -> held")
 
339
            assert isinstance(info, dict), \
 
340
                    "bad parse result %r" % info
418
341
            return info
419
342
        except NoSuchFile, e:
420
 
            self._trace("peek -> not held")
 
343
            return None
421
344
 
422
345
    def _prepare_info(self):
423
346
        """Write information about a pending lock to a temporary file.
440
363
    def _parse_info(self, info_file):
441
364
        return read_stanza(info_file.readlines()).as_dict()
442
365
 
443
 
    def attempt_lock(self):
444
 
        """Take the lock; fail if it's already held.
445
 
        
446
 
        If you wish to block until the lock can be obtained, call wait_lock()
447
 
        instead.
448
 
 
449
 
        :return: The lock token.
450
 
        :raises LockContention: if the lock is held by someone else.
451
 
        """
452
 
        if self._fake_read_lock:
453
 
            raise LockContention(self)
454
 
        return self._attempt_lock()
455
 
 
456
 
    def wait_lock(self, timeout=None, poll=None, max_attempts=None):
 
366
    def wait_lock(self, timeout=None, poll=None):
457
367
        """Wait a certain period for a lock.
458
368
 
459
369
        If the lock can be acquired within the bounded time, it
461
371
        is raised.  Either way, this function should return within
462
372
        approximately `timeout` seconds.  (It may be a bit more if
463
373
        a transport operation takes a long time to complete.)
464
 
 
465
 
        :param timeout: Approximate maximum amount of time to wait for the
466
 
        lock, in seconds.
467
 
         
468
 
        :param poll: Delay in seconds between retrying the lock.
469
 
 
470
 
        :param max_attempts: Maximum number of times to try to lock.
471
 
 
472
 
        :return: The lock token.
473
374
        """
474
375
        if timeout is None:
475
376
            timeout = _DEFAULT_TIMEOUT_SECONDS
476
377
        if poll is None:
477
378
            poll = _DEFAULT_POLL_SECONDS
478
 
        # XXX: the transport interface doesn't let us guard against operations
479
 
        # there taking a long time, so the total elapsed time or poll interval
480
 
        # may be more than was requested.
 
379
 
 
380
        # XXX: the transport interface doesn't let us guard 
 
381
        # against operations there taking a long time.
481
382
        deadline = time.time() + timeout
482
383
        deadline_str = None
483
384
        last_info = None
484
 
        attempt_count = 0
485
385
        while True:
486
 
            attempt_count += 1
487
386
            try:
488
 
                return self.attempt_lock()
 
387
                self.attempt_lock()
 
388
                return
489
389
            except LockContention:
490
 
                # possibly report the blockage, then try again
491
390
                pass
492
 
            # TODO: In a few cases, we find out that there's contention by
493
 
            # reading the held info and observing that it's not ours.  In
494
 
            # those cases it's a bit redundant to read it again.  However,
495
 
            # the normal case (??) is that the rename fails and so we
496
 
            # don't know who holds the lock.  For simplicity we peek
497
 
            # always.
498
391
            new_info = self.peek()
 
392
            mutter('last_info: %s, new info: %s', last_info, new_info)
499
393
            if new_info is not None and new_info != last_info:
500
394
                if last_info is None:
501
395
                    start = 'Unable to obtain'
506
400
                if deadline_str is None:
507
401
                    deadline_str = time.strftime('%H:%M:%S',
508
402
                                                 time.localtime(deadline))
509
 
                lock_url = self.transport.abspath(self.path)
510
403
                self._report_function('%s %s\n'
511
404
                                      '%s\n' # held by
512
405
                                      '%s\n' # locked ... ago
513
 
                                      'Will continue to try until %s, unless '
514
 
                                      'you press Ctrl-C\n'
515
 
                                      'If you\'re sure that it\'s not being '
516
 
                                      'modified, use bzr break-lock %s',
 
406
                                      'Will continue to try until %s\n',
517
407
                                      start,
518
408
                                      formatted_info[0],
519
409
                                      formatted_info[1],
520
410
                                      formatted_info[2],
521
 
                                      deadline_str,
522
 
                                      lock_url)
 
411
                                      deadline_str)
523
412
 
524
 
            if (max_attempts is not None) and (attempt_count >= max_attempts):
525
 
                self._trace("exceeded %d attempts")
526
 
                raise LockContention(self)
527
413
            if time.time() + poll < deadline:
528
 
                self._trace("waiting %ss", poll)
529
414
                time.sleep(poll)
530
415
            else:
531
 
                self._trace("timeout after waiting %ss", timeout)
532
416
                raise LockContention(self)
533
 
    
534
 
    def leave_in_place(self):
535
 
        self._locked_via_token = True
536
 
 
537
 
    def dont_leave_in_place(self):
538
 
        self._locked_via_token = False
539
 
 
540
 
    def lock_write(self, token=None):
541
 
        """Wait for and acquire the lock.
542
 
        
543
 
        :param token: if this is already locked, then lock_write will fail
544
 
            unless the token matches the existing lock.
545
 
        :returns: a token if this instance supports tokens, otherwise None.
546
 
        :raises TokenLockingNotSupported: when a token is given but this
547
 
            instance doesn't support using token locks.
548
 
        :raises MismatchedToken: if the specified token doesn't match the token
549
 
            of the existing lock.
550
 
 
551
 
        A token should be passed in if you know that you have locked the object
552
 
        some other way, and need to synchronise this object's state with that
553
 
        fact.
554
 
         
555
 
        XXX: docstring duplicated from LockableFiles.lock_write.
556
 
        """
557
 
        if token is not None:
558
 
            self.validate_token(token)
559
 
            self.nonce = token
560
 
            self._lock_held = True
561
 
            self._locked_via_token = True
562
 
            return token
563
 
        else:
564
 
            return self.wait_lock()
 
417
 
 
418
    def lock_write(self):
 
419
        """Wait for and acquire the lock."""
 
420
        self.wait_lock()
565
421
 
566
422
    def lock_read(self):
567
423
        """Compatibility-mode shared lock.
578
434
            raise LockContention(self)
579
435
        self._fake_read_lock = True
580
436
 
 
437
    def wait(self, timeout=20, poll=0.5):
 
438
        """Wait a certain period for a lock to be released."""
 
439
        # XXX: the transport interface doesn't let us guard 
 
440
        # against operations there taking a long time.
 
441
        deadline = time.time() + timeout
 
442
        while True:
 
443
            if self.peek():
 
444
                return
 
445
            if time.time() + poll < deadline:
 
446
                time.sleep(poll)
 
447
            else:
 
448
                raise LockContention(self)
 
449
 
581
450
    def _format_lock_info(self, info):
582
451
        """Turn the contents of peek() into something for the user"""
583
452
        lock_url = self.transport.abspath(self.path)
588
457
            'locked %s' % (format_delta(delta),),
589
458
            ]
590
459
 
591
 
    def validate_token(self, token):
592
 
        if token is not None:
593
 
            info = self.peek()
594
 
            if info is None:
595
 
                # Lock isn't held
596
 
                lock_token = None
597
 
            else:
598
 
                lock_token = info.get('nonce')
599
 
            if token != lock_token:
600
 
                raise errors.TokenMismatch(token, lock_token)
601
 
            else:
602
 
                self._trace("revalidated by token %r", token)
603
 
 
604
 
    def _trace(self, format, *args):
605
 
        if 'lock' not in debug.debug_flags:
606
 
            return
607
 
        mutter(str(self) + ": " + (format % args))