Created
August 27, 2009 02:04
-
-
Save danmackinlay/176027 to your computer and use it in GitHub Desktop.
django- or FS-based contextmanager-friendly locks for python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
various ways of acquiring cross-process locks on a string including a django one | |
http://www.evanfosmark.com/2009/01/cross-platform-file-locking-support-in-python/ | |
needs test coverage, but won't get it here since I've just removed it from the project. | |
`with` statement requires python 2.5 or later; these may work as non-with statement doohickeys | |
django locks require version 1.0 or later to get the "force_insert" argument | |
""" | |
import os | |
import errno | |
class BadName(Exception): | |
""" | |
raise when the lock string requested is invalid for the locking method | |
""" | |
pass | |
class LockException(Exception): | |
pass | |
class FileLock(object): | |
""" | |
A file locking mechanism that has context-manager support so | |
you can use it in a with statement. This should be relatively cross | |
compatible as it doesn't rely on msvcrt or fcntl for the locking. | |
""" | |
def __init__(self, name): | |
from django.conf import settings | |
lock_path = settings.TICKET_LOCK_PATH | |
""" | |
Prepare the file locker. Specify the file to lock. Do some | |
sanity checks to make sure you ain't messing with the wrong | |
bits of the FS. | |
this should surely be farmed off to an OS-specific library. | |
""" | |
if name.find("/")>-1: raise BadName("%s is not a safe name to lock on the FS") | |
if name.find("\\")>-1: raise BadName("%s is not a safe name to lock on the FS") | |
if name.find("..")>-1: raise BadName("%s is not a safe name to lock on the FS") | |
# if name.find("|")>-1: raise BadName("%s a bad name, since '|' is reserved as a separator") | |
self.is_locked = False | |
self.lockfile = os.path.join(lock_path, os.getcwd(), "%s" % name) | |
self.name = name | |
def acquire(self): | |
""" | |
Acquire the lock, if possible | |
""" | |
try: | |
self.fd = os.open(self.lockfile, os.O_CREAT|os.O_EXCL|os.O_RDWR) | |
self.is_locked = True | |
except OSError as e: | |
raise FileLockException("That was locked. stoppit.") | |
def release(self): | |
""" | |
Get rid of the lock by deleting the lockfile. | |
When working in a `with` statement, this gets automatically | |
called at the end. | |
""" | |
if self.is_locked: | |
os.close(self.fd) | |
os.unlink(self.lockfile) | |
self.is_locked = False | |
def __enter__(self): | |
""" | |
Activated when used in the with statement. | |
Should automatically acquire a lock to be used in the with block. | |
""" | |
if not self.is_locked: | |
self.acquire() | |
return self | |
def __exit__(self, type, value, traceback): | |
""" | |
Activated at the end of the with statement. | |
It automatically releases the lock if it isn't locked. | |
""" | |
if self.is_locked: | |
self.release() | |
def __del__(self): | |
""" | |
Make sure that the FileLock instance doesn't leave a lockfile | |
lying around. | |
""" | |
self.release() | |
class DummyLock(object): | |
""" | |
convenience dummy lock that always allows you the lock | |
""" | |
def __init__(self, *args, **kwargs): | |
pass | |
def __enter__(self): | |
pass | |
def __exit__(self, type, value, traceback): | |
pass | |
class DBLock(FileLock): | |
""" | |
this lock uses the | |
""" | |
def __init__(self, name): | |
self.is_locked = False | |
self.name = name | |
def acquire(self): | |
""" | |
Acquire the lock, if possible, by locking in the DB. This relies on a minimal DB lock class that looks like this | |
class TicketLock(models.Model): | |
''' | |
minimal lock model | |
''' | |
name = models.CharField(primary_key=True, unique=True, max_length=128, blank=False) | |
created = models.DateTimeField(auto_now=1, editable=False) | |
""" | |
from ticket.models import TicketLock | |
try: | |
lock = TicketLock(name=self.name) | |
self.lock.save(force_insert=True) | |
self.lock = lock | |
self.is_locked = True | |
except Exception as e: | |
#Horrible hack. but db errors have no shared base: | |
if e.__class__.__name__ == 'IntegrityError': | |
raise LockException("%s was locked." % self.name) | |
else: | |
raise | |
def release(self): | |
""" | |
Get rid of the lock by deleting the lockfile. | |
When working in a `with` statement, this gets automatically | |
called at the end. | |
""" | |
if self.is_locked: | |
self.lock.delete() | |
self.is_locked = False | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment