Login

Model Locking Mixin & Decorator (MySQL Advisory Locks)

Author:
pio
Posted:
May 20, 2011
Language:
Python
Version:
1.3
Score:
0 (after 0 ratings)

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

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 4 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Please login first before commenting.