Source

moin-2.0 / build / lib / MoinMoin / util / lock.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# Copyright: 2005 Florian Festi, Nir Soffer
# Copyright: 2008 MoinMoin:ThomasWaldmann
# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.

"""
    MoinMoin - locking functions
"""


import os, sys, tempfile, time, errno

from MoinMoin import log
logging = log.getLogger(__name__)

from MoinMoin.util import filesys

class Timer:
    """ Simple count down timer

    Useful for code that needs to complete a task within some timeout.
    """
    defaultSleep = 0.25
    maxSleep = 0.25

    def __init__(self, timeout):
        self.setTimeout(timeout)
        self._start = None
        self._stop = None

    def setTimeout(self, timeout):
        self.timeout = timeout
        if timeout is None:
            self._sleep = self.defaultSleep
        else:
            self._sleep = min(timeout / 10.0, self.maxSleep)

    def start(self):
        """ Start the countdown """
        if self.timeout is None:
            return
        now = time.time()
        self._start = now
        self._stop = now + self.timeout

    def haveTime(self):
        """ Check if timeout has not passed """
        if self.timeout is None:
            return True
        return time.time() <= self._stop

    def sleep(self):
        """ Sleep without sleeping over timeout """
        if self._stop is not None:
            timeLeft = max(self._stop - time.time(), 0)
            sleep = min(self._sleep, timeLeft)
        else:
            sleep = self._sleep
        time.sleep(sleep)

    def elapsed(self):
        return time.time() - self._start


class ExclusiveLock:
    """ Exclusive lock

    Uses a directory as portable lock method. On all platforms,
    creating a directory will fail if the directory exists.

    Only one exclusive lock per resource is allowed. This lock is not
    used directly by clients, but used by both ReadLock and WriteLock.

    If created with a timeout, the lock will expire timeout seconds
    after it has been acquired. Without a timeout, it will never expire.
    """
    fileName = '' # The directory is the lockDir
    timerClass = Timer

    def __init__(self, dir, timeout=None):
        """ Init a write lock

        :param dir: the lock directory. Since this lock uses a empty
            filename, the dir is the lockDir.
        :param timeout: while trying to acquire, the lock will expire
            other exclusive locks older than timeout.
            WARNING: because of file system timing limitations, timeouts
            must be at least 2 seconds.
        """
        self.dir = dir
        if timeout is not None and timeout < 2.0:
            raise ValueError('timeout must be at least 2 seconds')
        self.timeout = timeout
        if self.fileName:
            self.lockDir = os.path.join(dir, self.fileName)
            self._makeDir()
        else:
            self.lockDir = dir
        self._locked = False

    def acquire(self, timeout=None):
        """ Try to acquire a lock.

        Try to create the lock directory. If it fails because another
        lock exists, try to expire the other lock. Repeat after little
        sleep until timeout passed.

        Return True if a lock was acquired; False otherwise.
        """
        timer = self.timerClass(timeout)
        timer.start()
        while timer.haveTime():
            try:
                filesys.mkdir(self.lockDir)
                self._locked = True
                logging.debug('acquired exclusive lock: {0}'.format(self.lockDir, ))
                return True
            except OSError as err:
                if err.errno != errno.EEXIST:
                    raise
                if self.expire():
                    continue # Try immediately to acquire
                timer.sleep()
        logging.debug('failed to acquire exclusive lock: {0}'.format(self.lockDir, ))
        return False

    def release(self):
        """ Release the lock """
        if not self._locked:
            raise RuntimeError('lock already released: {0}'.format(self.lockDir))
        self._removeLockDir()
        self._locked = False
        logging.debug('released lock: {0}'.format(self.lockDir))

    def isLocked(self):
        return self._locked

    def exists(self):
        return os.path.exists(self.lockDir)

    def isExpired(self):
        """ Return True if too old or missing; False otherwise

        TODO: Since stat returns times using whole seconds, this is
        quite broken. Maybe use OS specific calls like Carbon.File on
        Mac OS X?
        """
        if self.timeout is None:
            return not self.exists()
        try:
            lock_age = time.time() - filesys.stat(self.lockDir).st_mtime
            return lock_age > self.timeout
        except OSError as err:
            if err.errno == errno.ENOENT:
                # No such lock file, therefore "expired"
                return True
            raise

    def expire(self):
        """ Return True if the lock is expired or missing; False otherwise. """
        if self.isExpired():
            self._removeLockDir()
            logging.debug("expired lock: {0}".format(self.lockDir))
            return True
        return False

    # Private -------------------------------------------------------

    def _makeDir(self):
        """ Make sure directory exists """
        try:
            filesys.mkdir(self.dir)
            logging.debug('created directory: {0}'.format(self.dir))
        except OSError as err:
            if err.errno != errno.EEXIST:
                raise

    def _removeLockDir(self):
        """ Remove lockDir ignoring 'No such file or directory' errors """
        try:
            filesys.rmdir(self.lockDir)
            logging.debug('removed directory: {0}'.format(self.dir))
        except OSError as err:
            if err.errno != errno.ENOENT:
                raise


class WriteLock(ExclusiveLock):
    """ Exclusive Read/Write Lock

    When a resource is locked with this lock, clients can't read
    or write the resource.

    This super-exclusive lock can't be acquired if there are any other
    locks, either WriteLock or ReadLocks. When trying to acquire, this
    lock will try to expire all existing ReadLocks.
    """
    fileName = 'write_lock'

    def __init__(self, dir, timeout=None, readlocktimeout=None):
        """ Init a write lock

        :param dir: the lock directory. Every resource should have one
            lock directory, which may contain read or write locks.
        :param timeout: while trying to acquire, the lock will expire
            other unreleased write locks older than timeout.
        :param readlocktimeout: while trying to acquire, the lock will
            expire other read locks older than readlocktimeout.
        """
        ExclusiveLock.__init__(self, dir, timeout)
        if readlocktimeout is None:
            self.readlocktimeout = timeout
        else:
            self.readlocktimeout = readlocktimeout

    def acquire(self, timeout=None):
        """ Acquire an exclusive write lock

        Try to acquire an exclusive lock, then try to expire existing
        read locks. If timeout has not passed, the lock is acquired.
        Otherwise, the exclusive lock is released and the lock is not
        acquired.

        Return True if lock acquired, False otherwise.
        """
        if self._locked:
            raise RuntimeError("lock already locked")
        result = False
        timer = self.timerClass(timeout)
        timer.start()
        if ExclusiveLock.acquire(self, timeout):
            try:
                while timer.haveTime():
                    self._expireReadLocks()
                    if not self._haveReadLocks():
                        result = timer.haveTime()
                        break
                    timer.sleep()
            finally:
                if result:
                    logging.debug('acquired write lock: {0}'.format(self.lockDir))
                    return True
                else:
                    self.release()
        return False

    # Private -------------------------------------------------------

    def _expireReadLocks(self):
        """ Expire old read locks """
        readLockFileName = ReadLock.fileName
        for name in os.listdir(self.dir):
            if not name.startswith(readLockFileName):
                continue
            LockDir = os.path.join(self.dir, name)
            ExclusiveLock(LockDir, self.readlocktimeout).expire()

    def _haveReadLocks(self):
        """ Return True if read locks exists; False otherwise """
        readLockFileName = ReadLock.fileName
        for name in os.listdir(self.dir):
            if name.startswith(readLockFileName):
                return True
        return False

class ReadLock(ExclusiveLock):
    """ Read lock

    The purpose of this lock is to mark the resource as read only.
    Multiple ReadLocks can be acquired for same resource, but no
    WriteLock can be acquired until all ReadLocks are released.

    Allows only one lock per instance.
    """
    fileName = 'read_lock_'

    def __init__(self, dir, timeout=None):
        """ Init a read lock

        :param dir: the lock directory. Every resource should have one
            lock directory, which may contain read or write locks.
        :param timeout: while trying to acquire, the lock will expire
            other unreleased write locks older than timeout.
        """
        ExclusiveLock.__init__(self, dir, timeout)
        writeLockDir = os.path.join(self.dir, WriteLock.fileName)
        self.writeLock = ExclusiveLock(writeLockDir, timeout)

    def acquire(self, timeout=None):
        """ Try to acquire a 'read' lock

        To prevent race conditions, acquire first an exclusive lock,
        then acquire a read lock. Finally release the exclusive lock so
        other can have read lock, too.
        """
        if self._locked:
            raise RuntimeError("lock already locked")
        if self.writeLock.acquire(timeout):
            try:
                self.lockDir = tempfile.mkdtemp('', self.fileName, self.dir)
                self._locked = True
                logging.debug('acquired read lock: {0}'.format(self.lockDir))
                return True
            finally:
                self.writeLock.release()
        return False


class LazyReadLock(ReadLock):
    """ Lazy Read lock

    See ReadLock, but we do an optimization here:
    If (and ONLY if) the resource protected by this lock is updated in a POSIX
    style "write new content to tmpfile, rename tmpfile -> origfile", then reading
    from an open origfile handle will give either the old content (when opened
    before the rename happens) or the new content (when opened after the rename
    happened), but never cause any trouble. This means that we don't have to lock
    at all in that case.

    Of course this doesn't work for us on the win32 platform:

    * using MoveFileEx requires opening the file with some FILE_SHARE_DELETE
      mode - we currently don't do that

    We currently solve by using the non-lazy locking code in ReadLock class.
    """
    def __init__(self, dir, timeout=None):
        if sys.platform == 'win32':
            ReadLock.__init__(self, dir, timeout)
        else: # POSIX
            self._locked = False

    def acquire(self, timeout=None):
        if sys.platform == 'win32':
            return ReadLock.acquire(self, timeout)
        else: # POSIX
            self._locked = True
            return True

    def release(self):
        if sys.platform == 'win32':
            return ReadLock.release(self)
        else:  # POSIX
            self._locked = False

    def exists(self):
        if sys.platform == 'win32':
            return ReadLock.exists(self)
        else: # POSIX
            return True

    def isExpired(self):
        if sys.platform == 'win32':
            return ReadLock.isExpired(self)
        else: # POSIX
            return True

    def expire(self):
        if sys.platform == 'win32':
            return ReadLock.expire(self)
        else: # POSIX
            return True

class LazyWriteLock(WriteLock):
    """ Lazy Write lock

    See WriteLock and LazyReadLock docs.
    """
    def __init__(self, dir, timeout=None):
        if sys.platform == 'win32':
            WriteLock.__init__(self, dir, timeout)
        else: # POSIX
            self._locked = False

    def acquire(self, timeout=None):
        if sys.platform == 'win32':
            return WriteLock.acquire(self, timeout)
        else: # POSIX
            self._locked = True
            return True

    def release(self):
        if sys.platform == 'win32':
            return WriteLock.release(self)
        else:  # POSIX
            self._locked = False

    def exists(self):
        if sys.platform == 'win32':
            return WriteLock.exists(self)
        else: # POSIX
            return True

    def isExpired(self):
        if sys.platform == 'win32':
            return WriteLock.isExpired(self)
        else: # POSIX
            return True

    def expire(self):
        if sys.platform == 'win32':
            return WriteLock.expire(self)
        else: # POSIX
            return True