Back to index

moin  1.9.0~rc2
lock.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - locking functions
00004 
00005     @copyright: 2005 Florian Festi, Nir Soffer,
00006                 2008 MoinMoin:ThomasWaldmann
00007     @license: GNU GPL, see COPYING for details.
00008 """
00009 
00010 import os, sys, tempfile, time, errno
00011 
00012 from MoinMoin import log
00013 logging = log.getLogger(__name__)
00014 
00015 from MoinMoin.util import filesys
00016 
00017 class Timer:
00018     """ Simple count down timer
00019 
00020     Useful for code that needs to complete a task within some timeout.
00021     """
00022     defaultSleep = 0.25
00023     maxSleep = 0.25
00024 
00025     def __init__(self, timeout):
00026         self.setTimeout(timeout)
00027         self._start = None
00028         self._stop = None
00029 
00030     def setTimeout(self, timeout):
00031         self.timeout = timeout
00032         if timeout is None:
00033             self._sleep = self.defaultSleep
00034         else:
00035             self._sleep = min(timeout / 10.0, self.maxSleep)
00036 
00037     def start(self):
00038         """ Start the countdown """
00039         if self.timeout is None:
00040             return
00041         now = time.time()
00042         self._start = now
00043         self._stop = now + self.timeout
00044 
00045     def haveTime(self):
00046         """ Check if timeout has not passed """
00047         if self.timeout is None:
00048             return True
00049         return time.time() <= self._stop
00050 
00051     def sleep(self):
00052         """ Sleep without sleeping over timeout """
00053         if self._stop is not None:
00054             timeLeft = max(self._stop - time.time(), 0)
00055             sleep = min(self._sleep, timeLeft)
00056         else:
00057             sleep = self._sleep
00058         time.sleep(sleep)
00059 
00060     def elapsed(self):
00061         return time.time() - self._start
00062 
00063 
00064 class ExclusiveLock:
00065     """ Exclusive lock
00066 
00067     Uses a directory as portable lock method. On all platforms,
00068     creating a directory will fail if the directory exists.
00069 
00070     Only one exclusive lock per resource is allowed. This lock is not
00071     used directly by clients, but used by both ReadLock and WriteLock.
00072 
00073     If created with a timeout, the lock will expire timeout seconds
00074     after it has been acquired. Without a timeout, it will never expire.
00075     """
00076     fileName = '' # The directory is the lockDir
00077     timerClass = Timer
00078 
00079     def __init__(self, dir, timeout=None):
00080         """ Init a write lock
00081 
00082         @param dir: the lock directory. Since this lock uses a empty
00083             filename, the dir is the lockDir.
00084         @param timeout: while trying to acquire, the lock will expire
00085             other exclusive locks older than timeout.
00086             WARNING: because of file system timing limitations, timeouts
00087             must be at least 2 seconds.
00088         """
00089         self.dir = dir
00090         if timeout is not None and timeout < 2.0:
00091             raise ValueError('timeout must be at least 2 seconds')
00092         self.timeout = timeout
00093         if self.fileName:
00094             self.lockDir = os.path.join(dir, self.fileName)
00095             self._makeDir()
00096         else:
00097             self.lockDir = dir
00098         self._locked = False
00099 
00100     def acquire(self, timeout=None):
00101         """ Try to acquire a lock.
00102 
00103         Try to create the lock directory. If it fails because another
00104         lock exists, try to expire the other lock. Repeat after little
00105         sleep until timeout passed.
00106 
00107         Return True if a lock was acquired; False otherwise.
00108         """
00109         timer = self.timerClass(timeout)
00110         timer.start()
00111         while timer.haveTime():
00112             try:
00113                 filesys.mkdir(self.lockDir)
00114                 self._locked = True
00115                 logging.debug('acquired exclusive lock: %s' % (self.lockDir, ))
00116                 return True
00117             except OSError, err:
00118                 if err.errno != errno.EEXIST:
00119                     raise
00120                 if self.expire():
00121                     continue # Try immediately to acquire
00122                 timer.sleep()
00123         logging.debug('failed to acquire exclusive lock: %s' % (self.lockDir, ))
00124         return False
00125 
00126     def release(self):
00127         """ Release the lock """
00128         if not self._locked:
00129             raise RuntimeError('lock already released: %s' % self.lockDir)
00130         self._removeLockDir()
00131         self._locked = False
00132         logging.debug('released lock: %s' % self.lockDir)
00133 
00134     def isLocked(self):
00135         return self._locked
00136 
00137     def exists(self):
00138         return os.path.exists(self.lockDir)
00139 
00140     def isExpired(self):
00141         """ Return True if too old or missing; False otherwise
00142 
00143         TODO: Since stat returns times using whole seconds, this is
00144         quite broken. Maybe use OS specific calls like Carbon.File on
00145         Mac OS X?
00146         """
00147         if self.timeout is None:
00148             return not self.exists()
00149         try:
00150             lock_age = time.time() - filesys.stat(self.lockDir).st_mtime
00151             return lock_age > self.timeout
00152         except OSError, err:
00153             if err.errno == errno.ENOENT:
00154                 # No such lock file, therefore "expired"
00155                 return True
00156             raise
00157 
00158     def expire(self):
00159         """ Return True if the lock is expired or missing; False otherwise. """
00160         if self.isExpired():
00161             self._removeLockDir()
00162             logging.debug("expired lock: %s" % self.lockDir)
00163             return True
00164         return False
00165 
00166     # Private -------------------------------------------------------
00167 
00168     def _makeDir(self):
00169         """ Make sure directory exists """
00170         try:
00171             filesys.mkdir(self.dir)
00172             logging.debug('created directory: %s' % self.dir)
00173         except OSError, err:
00174             if err.errno != errno.EEXIST:
00175                 raise
00176 
00177     def _removeLockDir(self):
00178         """ Remove lockDir ignoring 'No such file or directory' errors """
00179         try:
00180             filesys.rmdir(self.lockDir)
00181             logging.debug('removed directory: %s' % self.dir)
00182         except OSError, err:
00183             if err.errno != errno.ENOENT:
00184                 raise
00185 
00186 
00187 class WriteLock(ExclusiveLock):
00188     """ Exclusive Read/Write Lock
00189 
00190     When a resource is locked with this lock, clients can't read
00191     or write the resource.
00192 
00193     This super-exclusive lock can't be acquired if there are any other
00194     locks, either WriteLock or ReadLocks. When trying to acquire, this
00195     lock will try to expire all existing ReadLocks.
00196     """
00197     fileName = 'write_lock'
00198 
00199     def __init__(self, dir, timeout=None, readlocktimeout=None):
00200         """ Init a write lock
00201 
00202         @param dir: the lock directory. Every resource should have one
00203             lock directory, which may contain read or write locks.
00204         @param timeout: while trying to acquire, the lock will expire
00205             other unreleased write locks older than timeout.
00206         @param readlocktimeout: while trying to acquire, the lock will
00207             expire other read locks older than readlocktimeout.
00208         """
00209         ExclusiveLock.__init__(self, dir, timeout)
00210         if readlocktimeout is None:
00211             self.readlocktimeout = timeout
00212         else:
00213             self.readlocktimeout = readlocktimeout
00214 
00215     def acquire(self, timeout=None):
00216         """ Acquire an exclusive write lock
00217 
00218         Try to acquire an exclusive lock, then try to expire existing
00219         read locks. If timeout has not passed, the lock is acquired.
00220         Otherwise, the exclusive lock is released and the lock is not
00221         acquired.
00222 
00223         Return True if lock acquired, False otherwise.
00224         """
00225         if self._locked:
00226             raise RuntimeError("lock already locked")
00227         result = False
00228         timer = self.timerClass(timeout)
00229         timer.start()
00230         if ExclusiveLock.acquire(self, timeout):
00231             try:
00232                 while timer.haveTime():
00233                     self._expireReadLocks()
00234                     if not self._haveReadLocks():
00235                         result = timer.haveTime()
00236                         break
00237                     timer.sleep()
00238             finally:
00239                 if result:
00240                     logging.debug('acquired write lock: %s' % self.lockDir)
00241                     return True
00242                 else:
00243                     self.release()
00244         return False
00245 
00246     # Private -------------------------------------------------------
00247 
00248     def _expireReadLocks(self):
00249         """ Expire old read locks """
00250         readLockFileName = ReadLock.fileName
00251         for name in os.listdir(self.dir):
00252             if not name.startswith(readLockFileName):
00253                 continue
00254             LockDir = os.path.join(self.dir, name)
00255             ExclusiveLock(LockDir, self.readlocktimeout).expire()
00256 
00257     def _haveReadLocks(self):
00258         """ Return True if read locks exists; False otherwise """
00259         readLockFileName = ReadLock.fileName
00260         for name in os.listdir(self.dir):
00261             if name.startswith(readLockFileName):
00262                 return True
00263         return False
00264 
00265 class ReadLock(ExclusiveLock):
00266     """ Read lock
00267 
00268     The purpose of this lock is to mark the resource as read only.
00269     Multiple ReadLocks can be acquired for same resource, but no
00270     WriteLock can be acquired until all ReadLocks are released.
00271 
00272     Allows only one lock per instance.
00273     """
00274     fileName = 'read_lock_'
00275 
00276     def __init__(self, dir, timeout=None):
00277         """ Init a read lock
00278 
00279         @param dir: the lock directory. Every resource should have one
00280             lock directory, which may contain read or write locks.
00281         @param timeout: while trying to acquire, the lock will expire
00282             other unreleased write locks older than timeout.
00283         """
00284         ExclusiveLock.__init__(self, dir, timeout)
00285         writeLockDir = os.path.join(self.dir, WriteLock.fileName)
00286         self.writeLock = ExclusiveLock(writeLockDir, timeout)
00287 
00288     def acquire(self, timeout=None):
00289         """ Try to acquire a 'read' lock
00290 
00291         To prevent race conditions, acquire first an exclusive lock,
00292         then acquire a read lock. Finally release the exclusive lock so
00293         other can have read lock, too.
00294         """
00295         if self._locked:
00296             raise RuntimeError("lock already locked")
00297         if self.writeLock.acquire(timeout):
00298             try:
00299                 self.lockDir = tempfile.mkdtemp('', self.fileName, self.dir)
00300                 self._locked = True
00301                 logging.debug('acquired read lock: %s' % self.lockDir)
00302                 return True
00303             finally:
00304                 self.writeLock.release()
00305         return False
00306 
00307 
00308 class LazyReadLock(ReadLock):
00309     """ Lazy Read lock
00310 
00311     See ReadLock, but we do an optimization here:
00312     If (and ONLY if) the resource protected by this lock is updated in a POSIX
00313     style "write new content to tmpfile, rename tmpfile -> origfile", then reading
00314     from an open origfile handle will give either the old content (when opened
00315     before the rename happens) or the new content (when opened after the rename
00316     happened), but never cause any trouble. This means that we don't have to lock
00317     at all in that case.
00318 
00319     Of course this doesn't work for us on the win32 platform:
00320     * using MoveFileEx requires opening the file with some FILE_SHARE_DELETE
00321       mode - we currently don't do that
00322     * Win 95/98/ME do not have MoveFileEx
00323     We currently solve by using the non-lazy locking code in ReadLock class.
00324     """
00325     def __init__(self, dir, timeout=None):
00326         if sys.platform == 'win32':
00327             ReadLock.__init__(self, dir, timeout)
00328         else: # POSIX
00329             self._locked = False
00330 
00331     def acquire(self, timeout=None):
00332         if sys.platform == 'win32':
00333             return ReadLock.acquire(self, timeout)
00334         else: # POSIX
00335             self._locked = True
00336             return True
00337 
00338     def release(self):
00339         if sys.platform == 'win32':
00340             return ReadLock.release(self)
00341         else:  # POSIX
00342             self._locked = False
00343 
00344     def exists(self):
00345         if sys.platform == 'win32':
00346             return ReadLock.exists(self)
00347         else: # POSIX
00348             return True
00349 
00350     def isExpired(self):
00351         if sys.platform == 'win32':
00352             return ReadLock.isExpired(self)
00353         else: # POSIX
00354             return True
00355 
00356     def expire(self):
00357         if sys.platform == 'win32':
00358             return ReadLock.expire(self)
00359         else: # POSIX
00360             return True
00361 
00362 class LazyWriteLock(WriteLock):
00363     """ Lazy Write lock
00364 
00365     See WriteLock and LazyReadLock docs.
00366     """
00367     def __init__(self, dir, timeout=None):
00368         if sys.platform == 'win32':
00369             WriteLock.__init__(self, dir, timeout)
00370         else: # POSIX
00371             self._locked = False
00372 
00373     def acquire(self, timeout=None):
00374         if sys.platform == 'win32':
00375             return WriteLock.acquire(self, timeout)
00376         else: # POSIX
00377             self._locked = True
00378             return True
00379 
00380     def release(self):
00381         if sys.platform == 'win32':
00382             return WriteLock.release(self)
00383         else:  # POSIX
00384             self._locked = False
00385 
00386     def exists(self):
00387         if sys.platform == 'win32':
00388             return WriteLock.exists(self)
00389         else: # POSIX
00390             return True
00391 
00392     def isExpired(self):
00393         if sys.platform == 'win32':
00394             return WriteLock.isExpired(self)
00395         else: # POSIX
00396             return True
00397 
00398     def expire(self):
00399         if sys.platform == 'win32':
00400             return WriteLock.expire(self)
00401         else: # POSIX
00402             return True