/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/lockdir.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-04-17 08:04:15 UTC
  • mfrom: (2423.1.2 benchmarks)
  • Revision ID: pqm@pqm.ubuntu.com-20070417080415-5vn25svmf95ki88z
Also clear SmartTCPServer hooks from TestCase._clear_hooks

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
96
96
 
97
97
import os
98
98
import time
99
 
from warnings import warn
100
 
from StringIO import StringIO
 
99
from cStringIO import StringIO
101
100
 
 
101
from bzrlib import (
 
102
    errors,
 
103
    )
102
104
import bzrlib.config
103
105
from bzrlib.errors import (
104
106
        DirectoryNotEmpty,
106
108
        LockBreakMismatch,
107
109
        LockBroken,
108
110
        LockContention,
109
 
        LockError,
110
111
        LockNotHeld,
111
112
        NoSuchFile,
 
113
        PathError,
112
114
        ResourceBusy,
113
115
        UnlockableTransport,
114
116
        )
 
117
from bzrlib.trace import mutter, note
115
118
from bzrlib.transport import Transport
116
 
from bzrlib.osutils import rand_chars
117
 
from bzrlib.rio import RioWriter, read_stanza, Stanza
 
119
from bzrlib.osutils import rand_chars, format_delta
 
120
from bzrlib.rio import read_stanza, Stanza
 
121
import bzrlib.ui
 
122
 
118
123
 
119
124
# XXX: At the moment there is no consideration of thread safety on LockDir
120
125
# objects.  This should perhaps be updated - e.g. if two threads try to take a
121
126
# lock at the same time they should *both* get it.  But then that's unlikely
122
127
# to be a good idea.
123
128
 
124
 
# TODO: Transport could offer a simpler put() method that avoids the
125
 
# rename-into-place for cases like creating the lock template, where there is
126
 
# no chance that the file already exists.
127
 
 
128
129
# TODO: Perhaps store some kind of note like the bzr command line in the lock
129
130
# info?
130
131
 
134
135
# TODO: Make sure to pass the right file and directory mode bits to all
135
136
# files/dirs created.
136
137
 
 
138
 
137
139
_DEFAULT_TIMEOUT_SECONDS = 300
138
 
_DEFAULT_POLL_SECONDS = 0.5
 
140
_DEFAULT_POLL_SECONDS = 1.0
 
141
 
139
142
 
140
143
class LockDir(object):
141
144
    """Write-lock guarding access to data."""
157
160
        self.transport = transport
158
161
        self.path = path
159
162
        self._lock_held = False
 
163
        self._locked_via_token = False
160
164
        self._fake_read_lock = False
161
165
        self._held_dir = path + '/held'
162
166
        self._held_info_path = self._held_dir + self.__INFO_NAME
163
167
        self._file_modebits = file_modebits
164
168
        self._dir_modebits = dir_modebits
165
 
        self.nonce = rand_chars(20)
 
169
 
 
170
        self._report_function = note
166
171
 
167
172
    def __repr__(self):
168
173
        return '%s(%s%s)' % (self.__class__.__name__,
171
176
 
172
177
    is_held = property(lambda self: self._lock_held)
173
178
 
174
 
    def create(self):
 
179
    def create(self, mode=None):
175
180
        """Create the on-disk lock.
176
181
 
177
182
        This is typically only called when the object/directory containing the 
179
184
        """
180
185
        if self.transport.is_readonly():
181
186
            raise UnlockableTransport(self.transport)
182
 
        self.transport.mkdir(self.path)
 
187
        self.transport.mkdir(self.path, mode=mode)
183
188
 
184
189
    def attempt_lock(self):
185
190
        """Take the lock; fail if it's already held.
193
198
            raise UnlockableTransport(self.transport)
194
199
        try:
195
200
            tmpname = '%s/pending.%s.tmp' % (self.path, rand_chars(20))
196
 
            self.transport.mkdir(tmpname)
197
 
            sio = StringIO()
198
 
            self._prepare_info(sio)
199
 
            sio.seek(0)
200
 
            self.transport.put(tmpname + self.__INFO_NAME, sio)
 
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
            self.nonce = rand_chars(20)
 
213
            info_bytes = self._prepare_info()
 
214
            # We use put_file_non_atomic because we just created a new unique
 
215
            # directory so we don't have to worry about files existing there.
 
216
            # We'll rename the whole directory into place to get atomic
 
217
            # properties
 
218
            self.transport.put_bytes_non_atomic(tmpname + self.__INFO_NAME,
 
219
                                                info_bytes)
 
220
 
201
221
            self.transport.rename(tmpname, self._held_dir)
202
222
            self._lock_held = True
203
223
            self.confirm()
204
 
            return
205
 
        except (DirectoryNotEmpty, FileExists, ResourceBusy), e:
206
 
            pass
207
 
        # fall through to here on contention
208
 
        raise LockContention(self)
 
224
        except errors.PermissionDenied:
 
225
            raise
 
226
        except (PathError, DirectoryNotEmpty, FileExists, ResourceBusy), e:
 
227
            mutter("contention on %r: %s", self, e)
 
228
            raise LockContention(self)
209
229
 
210
230
    def unlock(self):
211
231
        """Release a held lock
215
235
            return
216
236
        if not self._lock_held:
217
237
            raise LockNotHeld(self)
218
 
        # rename before deleting, because we can't atomically remove the whole
219
 
        # tree
220
 
        tmpname = '%s/releasing.%s.tmp' % (self.path, rand_chars(20))
221
 
        self.transport.rename(self._held_dir, tmpname)
222
 
        self._lock_held = False
223
 
        self.transport.delete(tmpname + self.__INFO_NAME)
224
 
        self.transport.rmdir(tmpname)
225
 
 
 
238
        if self._locked_via_token:
 
239
            self._locked_via_token = False
 
240
            self._lock_held = False
 
241
        else:
 
242
            # rename before deleting, because we can't atomically remove the
 
243
            # whole tree
 
244
            tmpname = '%s/releasing.%s.tmp' % (self.path, rand_chars(20))
 
245
            # gotta own it to unlock
 
246
            self.confirm()
 
247
            self.transport.rename(self._held_dir, tmpname)
 
248
            self._lock_held = False
 
249
            self.transport.delete(tmpname + self.__INFO_NAME)
 
250
            self.transport.rmdir(tmpname)
 
251
 
 
252
    def break_lock(self):
 
253
        """Break a lock not held by this instance of LockDir.
 
254
 
 
255
        This is a UI centric function: it uses the bzrlib.ui.ui_factory to
 
256
        prompt for input if a lock is detected and there is any doubt about
 
257
        it possibly being still active.
 
258
        """
 
259
        self._check_not_locked()
 
260
        holder_info = self.peek()
 
261
        if holder_info is not None:
 
262
            lock_info = '\n'.join(self._format_lock_info(holder_info))
 
263
            if bzrlib.ui.ui_factory.get_boolean("Break %s" % lock_info):
 
264
                self.force_break(holder_info)
 
265
        
226
266
    def force_break(self, dead_holder_info):
227
267
        """Release a lock held by another process.
228
268
 
241
281
        """
242
282
        if not isinstance(dead_holder_info, dict):
243
283
            raise ValueError("dead_holder_info: %r" % dead_holder_info)
244
 
        if self._lock_held:
245
 
            raise AssertionError("can't break own lock: %r" % self)
 
284
        self._check_not_locked()
246
285
        current_info = self.peek()
247
286
        if current_info is None:
248
287
            # must have been recently released
261
300
        self.transport.delete(broken_info_path)
262
301
        self.transport.rmdir(tmpname)
263
302
 
 
303
    def _check_not_locked(self):
 
304
        """If the lock is held by this instance, raise an error."""
 
305
        if self._lock_held:
 
306
            raise AssertionError("can't break own lock: %r" % self)
 
307
 
264
308
    def confirm(self):
265
309
        """Make sure that the lock is still held by this locker.
266
310
 
303
347
        except NoSuchFile, e:
304
348
            return None
305
349
 
306
 
    def _prepare_info(self, outf):
 
350
    def _prepare_info(self):
307
351
        """Write information about a pending lock to a temporary file.
308
352
        """
309
353
        import socket
310
354
        # XXX: is creating this here inefficient?
311
355
        config = bzrlib.config.GlobalConfig()
 
356
        try:
 
357
            user = config.user_email()
 
358
        except errors.NoEmailInUsername:
 
359
            user = config.username()
312
360
        s = Stanza(hostname=socket.gethostname(),
313
361
                   pid=str(os.getpid()),
314
362
                   start_time=str(int(time.time())),
315
363
                   nonce=self.nonce,
316
 
                   user=config.user_email(),
 
364
                   user=user,
317
365
                   )
318
 
        RioWriter(outf).write_stanza(s)
 
366
        return s.to_string()
319
367
 
320
368
    def _parse_info(self, info_file):
321
369
        return read_stanza(info_file.readlines()).as_dict()
322
370
 
323
 
    def wait_lock(self, timeout=_DEFAULT_TIMEOUT_SECONDS,
324
 
                  poll=_DEFAULT_POLL_SECONDS):
 
371
    def wait_lock(self, timeout=None, poll=None):
325
372
        """Wait a certain period for a lock.
326
373
 
327
374
        If the lock can be acquired within the bounded time, it
330
377
        approximately `timeout` seconds.  (It may be a bit more if
331
378
        a transport operation takes a long time to complete.)
332
379
        """
 
380
        if timeout is None:
 
381
            timeout = _DEFAULT_TIMEOUT_SECONDS
 
382
        if poll is None:
 
383
            poll = _DEFAULT_POLL_SECONDS
 
384
 
333
385
        # XXX: the transport interface doesn't let us guard 
334
386
        # against operations there taking a long time.
335
387
        deadline = time.time() + timeout
 
388
        deadline_str = None
 
389
        last_info = None
336
390
        while True:
337
391
            try:
338
392
                self.attempt_lock()
339
393
                return
340
394
            except LockContention:
341
395
                pass
 
396
            new_info = self.peek()
 
397
            mutter('last_info: %s, new info: %s', last_info, new_info)
 
398
            if new_info is not None and new_info != last_info:
 
399
                if last_info is None:
 
400
                    start = 'Unable to obtain'
 
401
                else:
 
402
                    start = 'Lock owner changed for'
 
403
                last_info = new_info
 
404
                formatted_info = self._format_lock_info(new_info)
 
405
                if deadline_str is None:
 
406
                    deadline_str = time.strftime('%H:%M:%S',
 
407
                                                 time.localtime(deadline))
 
408
                self._report_function('%s %s\n'
 
409
                                      '%s\n' # held by
 
410
                                      '%s\n' # locked ... ago
 
411
                                      'Will continue to try until %s\n',
 
412
                                      start,
 
413
                                      formatted_info[0],
 
414
                                      formatted_info[1],
 
415
                                      formatted_info[2],
 
416
                                      deadline_str)
 
417
 
342
418
            if time.time() + poll < deadline:
343
419
                time.sleep(poll)
344
420
            else:
345
421
                raise LockContention(self)
346
 
 
347
 
    def lock_write(self):
348
 
        """Wait for and acquire the lock."""
349
 
        self.attempt_lock()
 
422
    
 
423
    def leave_in_place(self):
 
424
        self._locked_via_token = True
 
425
 
 
426
    def dont_leave_in_place(self):
 
427
        self._locked_via_token = False
 
428
 
 
429
    def lock_write(self, token=None):
 
430
        """Wait for and acquire the lock.
 
431
        
 
432
        :param token: if this is already locked, then lock_write will fail
 
433
            unless the token matches the existing lock.
 
434
        :returns: a token if this instance supports tokens, otherwise None.
 
435
        :raises TokenLockingNotSupported: when a token is given but this
 
436
            instance doesn't support using token locks.
 
437
        :raises MismatchedToken: if the specified token doesn't match the token
 
438
            of the existing lock.
 
439
 
 
440
        A token should be passed in if you know that you have locked the object
 
441
        some other way, and need to synchronise this object's state with that
 
442
        fact.
 
443
         
 
444
        XXX: docstring duplicated from LockableFiles.lock_write.
 
445
        """
 
446
        if token is not None:
 
447
            self.validate_token(token)
 
448
            self.nonce = token
 
449
            self._lock_held = True
 
450
            self._locked_via_token = True
 
451
            return token
 
452
        else:
 
453
            self.wait_lock()
 
454
            return self.peek().get('nonce')
350
455
 
351
456
    def lock_read(self):
352
 
        """Compatability-mode shared lock.
 
457
        """Compatibility-mode shared lock.
353
458
 
354
459
        LockDir doesn't support shared read-only locks, so this 
355
460
        just pretends that the lock is taken but really does nothing.
376
481
            else:
377
482
                raise LockContention(self)
378
483
 
 
484
    def _format_lock_info(self, info):
 
485
        """Turn the contents of peek() into something for the user"""
 
486
        lock_url = self.transport.abspath(self.path)
 
487
        delta = time.time() - int(info['start_time'])
 
488
        return [
 
489
            'lock %s' % (lock_url,),
 
490
            'held by %(user)s on host %(hostname)s [process #%(pid)s]' % info,
 
491
            'locked %s' % (format_delta(delta),),
 
492
            ]
 
493
 
 
494
    def validate_token(self, token):
 
495
        if token is not None:
 
496
            info = self.peek()
 
497
            if info is None:
 
498
                # Lock isn't held
 
499
                lock_token = None
 
500
            else:
 
501
                lock_token = info.get('nonce')
 
502
            if token != lock_token:
 
503
                raise errors.TokenMismatch(token, lock_token)
 
504