Skip to content

Commit f074079

Browse files
committed
added db mutex code
1 parent 2787146 commit f074079

File tree

17 files changed

+802
-0
lines changed

17 files changed

+802
-0
lines changed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Compiled python files
2+
*.pyc
3+
4+
# Vim files
5+
*.swp
6+
*.swo
7+
8+
# Coverage files
9+
.coverage
10+
11+
# Setuptools distribution folder.
12+
/dist/
13+
14+
# Python egg metadata, regenerated from source files by setuptools.
15+
/*.egg-info
16+
/*.egg

.travis.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: python
2+
python:
3+
- '2.7'
4+
env:
5+
- DJANGO=1.6.1 DB=postgres
6+
install:
7+
- pip install -q Django==$DJANGO
8+
- pip install -r requirements.txt
9+
before_script:
10+
- find . | grep .py$ | grep -v /migrations | xargs pep8 --max-line-length=120
11+
- find . | grep .py$ | grep -v /migrations | grep -v __init__.py | xargs pyflakes
12+
- psql -c 'CREATE DATABASE db_mutex;' -U postgres
13+
script:
14+
- coverage run --source='db_mutex' --branch manage.py test
15+
- coverage report --fail-under=100

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include db_mutex/VERSION
2+
include README.md
3+
include LICENSE

db_mutex/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1

db_mutex/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .db_mutex import DBMutexError, DBMutexTimeoutError, db_mutex

db_mutex/db_mutex.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from datetime import datetime, timedelta
2+
import functools
3+
4+
from django.conf import settings
5+
from django.db import transaction, IntegrityError
6+
7+
from .models import DBMutex
8+
9+
10+
class DBMutexError(Exception):
11+
"""
12+
Thrown when a lock cannot be acquired.
13+
"""
14+
pass
15+
16+
17+
class DBMutexTimeoutError(Exception):
18+
"""
19+
Thrown when a lock times out before it is released.
20+
"""
21+
pass
22+
23+
24+
class db_mutex(object):
25+
"""
26+
An object that acts as a context manager and a function decorator for acquiring a
27+
DB mutex lock.
28+
29+
Args:
30+
lock_id: The ID of the lock one is trying to acquire
31+
32+
Raises:
33+
DBMutexError when the lock cannot be obtained
34+
DBMutexTimeoutError when the lock was deleted during execution
35+
36+
Examples:
37+
This context manager/function decorator can be used in the following way
38+
39+
from db_mutex import db_mutex
40+
41+
# Lock a critical section of code
42+
try:
43+
with db_mutex('lock_id'):
44+
# Run critical code here
45+
pass
46+
except DBMutexError:
47+
print 'Could not obtain lock'
48+
except DBMutexTimeoutError:
49+
print 'Task completed but the lock timed out'
50+
51+
# Lock a function
52+
@db_mutex('lock_id'):
53+
def critical_function():
54+
# Critical code goes here
55+
pass
56+
57+
try:
58+
critical_function()
59+
except DBMutexError:
60+
print 'Could not obtain lock'
61+
except DBMutexTimeoutError:
62+
print 'Task completed but the lock timed out'
63+
"""
64+
mutex_ttl_seconds_settings_key = 'DB_MUTEX_TTL_SECONDS'
65+
66+
def __init__(self, lock_id):
67+
self.lock_id = lock_id
68+
self.lock = None
69+
70+
def get_mutex_ttl_seconds(self):
71+
"""
72+
Returns a TTL for mutex locks. It defaults to 30 minutes. If the user specifies None
73+
as the TTL, locks never expire.
74+
"""
75+
if hasattr(settings, self.mutex_ttl_seconds_settings_key):
76+
return getattr(settings, self.mutex_ttl_seconds_settings_key)
77+
else:
78+
return 30 * 60
79+
80+
def delete_expired_locks(self):
81+
"""
82+
Deletes all expired mutex locks if a ttl is provided.
83+
"""
84+
ttl_seconds = self.get_mutex_ttl_seconds()
85+
if ttl_seconds is not None:
86+
DBMutex.objects.filter(creation_time__lte=datetime.utcnow() - timedelta(seconds=ttl_seconds)).delete()
87+
88+
def __call__(self, func):
89+
return self.decorate_callable(func)
90+
91+
def __enter__(self):
92+
self.start()
93+
94+
def __exit__(self, *args):
95+
self.stop()
96+
97+
def start(self):
98+
"""
99+
Acquires the db mutex lock. Takes the necessary steps to delete any stale locks.
100+
Throws a DBMutexError if it can't acquire the lock.
101+
"""
102+
# Delete any expired locks first
103+
self.delete_expired_locks()
104+
try:
105+
with transaction.atomic():
106+
self.lock = DBMutex.objects.create(lock_id=self.lock_id)
107+
except IntegrityError:
108+
raise DBMutexError('Could not acquire lock: {0}'.format(self.lock_id))
109+
110+
def stop(self):
111+
"""
112+
Releases the db mutex lock. Throws an error if the lock was released before the function finished.
113+
"""
114+
if not DBMutex.objects.filter(id=self.lock.id).exists():
115+
raise DBMutexTimeoutError('Lock {0} expired before function completed'.format(self.lock_id))
116+
else:
117+
self.lock.delete()
118+
119+
def decorate_callable(self, func):
120+
"""
121+
Decorates a function with the db_mutex decorator by using this class as a context manager around
122+
it.
123+
"""
124+
def wrapper(*args, **kwargs):
125+
with self:
126+
result = func(*args, **kwargs)
127+
return result
128+
functools.update_wrapper(wrapper, func)
129+
return wrapper

db_mutex/migrations/0001_initial.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# -*- coding: utf-8 -*-
2+
import datetime
3+
from south.db import db
4+
from south.v2 import SchemaMigration
5+
from django.db import models
6+
7+
8+
class Migration(SchemaMigration):
9+
10+
def forwards(self, orm):
11+
# Adding model 'DBMutex'
12+
db.create_table(u'db_mutex_dbmutex', (
13+
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14+
('lock_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=256)),
15+
('creation_time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
16+
))
17+
db.send_create_signal(u'db_mutex', ['DBMutex'])
18+
19+
20+
def backwards(self, orm):
21+
# Deleting model 'DBMutex'
22+
db.delete_table(u'db_mutex_dbmutex')
23+
24+
25+
models = {
26+
u'db_mutex.dbmutex': {
27+
'Meta': {'object_name': 'DBMutex'},
28+
'creation_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
29+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30+
'lock_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'})
31+
}
32+
}
33+
34+
complete_apps = ['db_mutex']

db_mutex/migrations/__init__.py

Whitespace-only changes.

db_mutex/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.db import models
2+
3+
4+
class DBMutex(models.Model):
5+
"""
6+
Models a mutex lock with a lock ID and a creation time.
7+
"""
8+
lock_id = models.CharField(max_length=256, unique=True)
9+
creation_time = models.DateTimeField(auto_now_add=True)

manage.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
5+
if __name__ == '__main__':
6+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings')
7+
8+
from django.core.management import execute_from_command_line
9+
10+
execute_from_command_line(sys.argv)

0 commit comments

Comments
 (0)