Skip to content

Commit c0bbe43

Browse files
authored
Locking backend with database (#185)
* Locking backend with database * documentation of database lock
1 parent 70d8ba6 commit c0bbe43

File tree

7 files changed

+78
-3
lines changed

7 files changed

+78
-3
lines changed

demo/demo/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,5 @@
127127
CRON_CLASSES = [
128128
"demo.cron.EmailUsercountCronJob",
129129
]
130+
# If you want to test django locking with database
131+
# DJANGO_CRON_LOCK_BACKEND = "django_cron.backends.lock.database.DatabaseLock"

django_cron/admin.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.db.models import F
55
from django.utils.translation import gettext_lazy as _
66

7-
from django_cron.models import CronJobLog
7+
from django_cron.models import CronJobLog, CronJobLock
88
from django_cron.helpers import humanize_duration
99

1010

@@ -54,3 +54,4 @@ def humanize_duration(self, obj):
5454

5555

5656
admin.site.register(CronJobLog, CronJobLogAdmin)
57+
admin.site.register(CronJobLock)

django_cron/backends/lock/database.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django_cron.backends.lock.base import DjangoCronJobLock
2+
from django_cron.models import CronJobLock
3+
from django.db import transaction
4+
5+
6+
class DatabaseLock(DjangoCronJobLock):
7+
"""
8+
Locking cron jobs with database. Its good when you have not parallel run and want to make sure 2 jobs won't be
9+
fired at the same time - which may happened when job execution is longer that job interval.
10+
"""
11+
12+
@transaction.atomic
13+
def lock(self):
14+
lock, created = CronJobLock.objects.get_or_create(job_name=self.job_name)
15+
if lock.locked:
16+
return False
17+
else:
18+
lock.locked = True
19+
lock.save()
20+
return True
21+
22+
@transaction.atomic
23+
def release(self):
24+
lock = CronJobLock.objects.filter(
25+
job_name=self.job_name,
26+
locked=True
27+
).first()
28+
lock.locked = False
29+
lock.save()
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('django_cron', '0002_remove_max_length_from_CronJobLog_message'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='CronJobLock',
16+
fields=[
17+
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18+
('job_name', models.CharField(max_length=200, unique=True)),
19+
('locked', models.BooleanField(default=False)),
20+
],
21+
),
22+
]

django_cron/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ class Meta:
2929
('code', 'start_time') # useful when finding latest run (order by start_time) of cron
3030
]
3131
app_label = 'django_cron'
32+
33+
34+
class CronJobLock(models.Model):
35+
job_name = models.CharField(max_length=200, unique=True)
36+
locked = models.BooleanField(default=False)

django_cron/tests.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from django.contrib.auth.models import User
1515

1616
from django_cron.helpers import humanize_duration
17-
from django_cron.models import CronJobLog
17+
from django_cron.models import CronJobLog, CronJobLock
1818
import test_crons
1919

2020

@@ -105,6 +105,17 @@ def test_file_locking_backend(self):
105105
self._call(self.success_cron, force=True)
106106
self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1)
107107

108+
@override_settings(DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.database.DatabaseLock')
109+
def test_database_locking_backend(self):
110+
# TODO: to test it properly we would need to run multiple jobs at the same time
111+
logs_count = CronJobLog.objects.all().count()
112+
cron_job_locks = CronJobLock.objects.all().count()
113+
for _ in range(3):
114+
call(self.success_cron, force=True)
115+
self.assertEqual(CronJobLog.objects.all().count(), logs_count + 3)
116+
self.assertEqual(CronJobLock.objects.all().count(), cron_job_locks + 1)
117+
self.assertEqual(CronJobLock.objects.first().locked, False)
118+
108119
@patch.object(test_crons.TestSuccessCronJob, 'do')
109120
def test_dry_run_does_not_perform_task(self, mock_do):
110121
response = self._call(self.success_cron, dry_run=True)

docs/locking_backend.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
Locking Backend
22
===============
33

4-
You can use one of two built-in locking backends by setting ``DJANGO_CRON_LOCK_BACKEND`` with one of:
4+
You can use one of three built-in locking backends by setting ``DJANGO_CRON_LOCK_BACKEND`` with one of:
55

66
- ``django_cron.backends.lock.cache.CacheLock`` (default)
77
- ``django_cron.backends.lock.file.FileLock``
8+
- ``django_cron.backends.lock.database.DatabaseLock``
89

910

1011
Cache Lock
@@ -16,6 +17,10 @@ File Lock
1617
---------
1718
This backend creates a file to mark current job as "already running", and delete it when lock is released.
1819

20+
Database Lock
21+
---------
22+
This backend creates new model for jobs, saving their state as locked when they starts, and setting it to unlocked when
23+
job is finished. It may help preventing multiple instances of the same job running.
1924

2025
Custom Lock
2126
-----------

0 commit comments

Comments
 (0)