Skip to content

Commit

Permalink
Locking backend with database (#185)
Browse files Browse the repository at this point in the history
* Locking backend with database

* documentation of database lock
  • Loading branch information
nerjan authored May 9, 2022
1 parent 70d8ba6 commit c0bbe43
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 3 deletions.
2 changes: 2 additions & 0 deletions demo/demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@
CRON_CLASSES = [
"demo.cron.EmailUsercountCronJob",
]
# If you want to test django locking with database
# DJANGO_CRON_LOCK_BACKEND = "django_cron.backends.lock.database.DatabaseLock"
3 changes: 2 additions & 1 deletion django_cron/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.db.models import F
from django.utils.translation import gettext_lazy as _

from django_cron.models import CronJobLog
from django_cron.models import CronJobLog, CronJobLock
from django_cron.helpers import humanize_duration


Expand Down Expand Up @@ -54,3 +54,4 @@ def humanize_duration(self, obj):


admin.site.register(CronJobLog, CronJobLogAdmin)
admin.site.register(CronJobLock)
29 changes: 29 additions & 0 deletions django_cron/backends/lock/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django_cron.backends.lock.base import DjangoCronJobLock
from django_cron.models import CronJobLock
from django.db import transaction


class DatabaseLock(DjangoCronJobLock):
"""
Locking cron jobs with database. Its good when you have not parallel run and want to make sure 2 jobs won't be
fired at the same time - which may happened when job execution is longer that job interval.
"""

@transaction.atomic
def lock(self):
lock, created = CronJobLock.objects.get_or_create(job_name=self.job_name)
if lock.locked:
return False
else:
lock.locked = True
lock.save()
return True

@transaction.atomic
def release(self):
lock = CronJobLock.objects.filter(
job_name=self.job_name,
locked=True
).first()
lock.locked = False
lock.save()
22 changes: 22 additions & 0 deletions django_cron/migrations/0003_cronjoblock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('django_cron', '0002_remove_max_length_from_CronJobLog_message'),
]

operations = [
migrations.CreateModel(
name='CronJobLock',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('job_name', models.CharField(max_length=200, unique=True)),
('locked', models.BooleanField(default=False)),
],
),
]
5 changes: 5 additions & 0 deletions django_cron/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ class Meta:
('code', 'start_time') # useful when finding latest run (order by start_time) of cron
]
app_label = 'django_cron'


class CronJobLock(models.Model):
job_name = models.CharField(max_length=200, unique=True)
locked = models.BooleanField(default=False)
13 changes: 12 additions & 1 deletion django_cron/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.contrib.auth.models import User

from django_cron.helpers import humanize_duration
from django_cron.models import CronJobLog
from django_cron.models import CronJobLog, CronJobLock
import test_crons


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

@override_settings(DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.database.DatabaseLock')
def test_database_locking_backend(self):
# TODO: to test it properly we would need to run multiple jobs at the same time
logs_count = CronJobLog.objects.all().count()
cron_job_locks = CronJobLock.objects.all().count()
for _ in range(3):
call(self.success_cron, force=True)
self.assertEqual(CronJobLog.objects.all().count(), logs_count + 3)
self.assertEqual(CronJobLock.objects.all().count(), cron_job_locks + 1)
self.assertEqual(CronJobLock.objects.first().locked, False)

@patch.object(test_crons.TestSuccessCronJob, 'do')
def test_dry_run_does_not_perform_task(self, mock_do):
response = self._call(self.success_cron, dry_run=True)
Expand Down
7 changes: 6 additions & 1 deletion docs/locking_backend.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
Locking Backend
===============

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

- ``django_cron.backends.lock.cache.CacheLock`` (default)
- ``django_cron.backends.lock.file.FileLock``
- ``django_cron.backends.lock.database.DatabaseLock``


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

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

Custom Lock
-----------
Expand Down

0 comments on commit c0bbe43

Please sign in to comment.