This code provides a mixin and decorator which, when used together, can provide advisory locking on model methods. It provides locking by using MySQL's advisory lock system. See the example at the bottom of the code.
This is a convenient and easy way to guarantee your model methods have exclusive access to a model instance.
The LockableObjects class requires a MySQL backend, but it could be modified to use other back-end locking systems.
The lock name generation in LockableObject.get_lock_name()
could be altered to create much more complex locking schemes. Locking per-object, per-method for example..
Lock attempts have a timeout value of 45 seconds by default. If a timeout occurs, EnvironmentError is raised.
See the bottom of the script for an example
Instructions:
-
1: Place the code in locking.py somewhere in your path
-
2: In your models.py (or any script with an object you want to lock):
from locking import LockableObject, require_object_lock
-
3: In the model you want locking for, add the
LockableObject
mixin -
4: Decorate the method you want to be exclusively locked with
@require_object_lock
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 | from django.db import connection
class LockableObject(object):
default_timeout = 45
def __init__(self, *args, **kwargs):
super(LockableObject, self).__init__(*args, **kwargs)
self.dbcursor = connection.cursor()
self.lock_id = None
def get_lock_name(self):
return '%s|%s' % (self.__class__.__name__,
self.lock_id)
def lock(self):
if hasattr(self, 'id'):
self.lock_id = self.id
else:
self.lock_id = 0
lock_name = self.get_lock_name()
self.dbcursor.execute('select get_lock("%s",%s) as lock_success' % (lock_name,
self.default_timeout))
success = ( self.dbcursor.fetchone()[0] == 1 )
if not success:
raise EnvironmentError, 'Acquiring lock "%s" timed out after %d seconds' % (lock_name, self.default_timeout)
return success
def unlock(self):
self.dbcursor.execute('select release_lock("%s")' % self.get_lock_name())
def require_object_lock(func):
def wrapped(*args, **kwargs):
lock_object = args[0]
lock_object.lock()
try:
return func(*args, **kwargs)
finally:
lock_object.unlock()
return wrapped
##########################################################################
# Example
from django.db import models
from django.core.mail import mail_admins
class Notification(models.Model, LockableObject):
message = models.CharField()
sent = models.BooleanField()
@require_object_lock
def send(self):
if not self.sent:
mail_admins('Notification',
self.message)
self.sent = True
self.save()
a = Notification(message='Hello world',
sent=False)
# Important to save; we can't lock a specific object without it having
# an 'id' attribute.
a.save()
a.send()
# Now, we are guaranteed that no matter how many threads try, and no
# matter their timing, calls to the send() method of this row's object
# can only generate ONE mail_admins() call. We've prevented the race
# condition of two threads calling send() at the same time and both of
# them seeing self.sent as False and sending the mail twice.
#
# Note that if mail_admins failed and threw an exception, the
# require_object_lock decorator catches the exception, releases the
# lock, then raises it to let it fulfill its normal behavior.
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Please login first before commenting.