Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e6c7c21
updated minor version
kggoyani192 Aug 31, 2018
1bfa94a
Fixed query to find latest cron job log
kggoyani192 Aug 31, 2018
baf5a35
1. Support for different cron classes via --run_class_list_name <list…
apabreja Jun 19, 2019
3ec366c
Make sure that the codes are stripped to remove return carriages
apabreja Aug 20, 2019
27ff3f2
lock changed get_code from code
kggoyani192 Dec 5, 2019
86575cf
get_code made classmethond
Aug 5, 2021
a99a6d4
byte + string error and vsrsion bump up
Oct 31, 2021
5712562
ec2 get public ip
Jan 29, 2022
0126678
stripping \n from public ip
Mar 9, 2022
b29b1ff
send failed cron email when cron fails
Apr 5, 2022
7b000e1
email on error while importing crons
Apr 6, 2022
fa701d3
version upgrade
Apr 6, 2022
caf572a
checking minimum failires before sending email
Apr 6, 2022
4953ebc
Added comments changed gitignore
apabreja Apr 8, 2022
5b28c3b
resolving conflict
codegeass2409 May 5, 2023
d475093
removing duplicate parser.add_argument
codegeass2409 May 9, 2023
15e03d8
update changes
codegeass2409 May 9, 2023
dae11fa
admin changes
codegeass2409 May 18, 2023
27b9339
replacing datetime.today() with get_current_time()
codegeass2409 May 18, 2023
3472b04
, error
codegeass2409 May 18, 2023
604236a
adding get_code()
codegeass2409 May 18, 2023
4868b96
removing editable false
codegeass2409 May 18, 2023
b74a49f
adding job_code
codegeass2409 May 19, 2023
354418c
adding locked in str and also logger will show job_code
codegeass2409 May 19, 2023
5899e02
.using(default) added
May 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,8 @@ db.sqlite3

# Vim
*.swp
#Amber cscope files
cscope.files
cscope.out
mk_cscope
tags
8 changes: 6 additions & 2 deletions django_cron/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Meta:

search_fields = ('code', 'message')
ordering = ('-start_time',)
list_display = ('code', 'start_time', 'end_time', 'humanize_duration', 'is_success')
list_display = ('code', 'start_time', 'end_time', 'humanize_duration', 'is_success', 'ran_at_time')
list_filter = ('code', 'start_time', 'is_success', DurationFilter)

def get_queryset(self, request):
Expand All @@ -58,5 +58,9 @@ def humanize_duration(self, obj):
humanize_duration.admin_order_field = 'duration'


class CronJobLockAdmin(admin.ModelAdmin):
list_display = ('pk', 'job_name', 'locked')
list_filter = ('locked',)

admin.site.register(CronJobLog, CronJobLogAdmin)
admin.site.register(CronJobLock)
admin.site.register(CronJobLock, CronJobLockAdmin)
4 changes: 2 additions & 2 deletions django_cron/backends/lock/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, cron_class, silent, *args, **kwargs):
for you. The rest is backend-specific.
"""
self.job_name = '.'.join([cron_class.__module__, cron_class.__name__])
self.job_code = cron_class.code
self.job_code = cron_class.get_code()
self.parallel = getattr(cron_class, 'ALLOW_PARALLEL_RUNS', False)
self.silent = silent

Expand All @@ -55,7 +55,7 @@ def release(self):
)

def lock_failed_message(self):
return "%s: lock found. Will try later." % self.job_name
return "%s: lock found. Will try later." % self.job_code

def __enter__(self):
if not self.parallel and not self.lock():
Expand Down
4 changes: 3 additions & 1 deletion django_cron/backends/lock/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def get_cache_by_name(self):
return caches[cache_name]

def get_lock_name(self):
return self.job_name
#We are using IP based codes. Name is Check_Documents_Cron ie the Class name. This is terrible since 2 apps can have the same class name
#Code is fmeca.check_documents_cron.203.23.34.56
return self.job_code

def get_cache_timeout(self, cron_class):
try:
Expand Down
9 changes: 5 additions & 4 deletions django_cron/backends/lock/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class DatabaseLock(DjangoCronJobLock):

@transaction.atomic
def lock(self):
lock, created = CronJobLock.objects.get_or_create(job_name=self.job_name)
lock, created = CronJobLock.objects.using("default").get_or_create(job_name=self.job_code)
if lock.locked:
return False
else:
Expand All @@ -21,6 +21,7 @@ def lock(self):

@transaction.atomic
def release(self):
lock = CronJobLock.objects.filter(job_name=self.job_name, locked=True).first()
lock.locked = False
lock.save()
lock = CronJobLock.objects.using("default").filter(job_name=self.job_code, locked=True).first()
if lock:
lock.locked = False
lock.save()
2 changes: 1 addition & 1 deletion django_cron/backends/lock/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ def get_lock_name(self):
# let it die if failed, can't run further anyway
os.makedirs(path, exist_ok=True)

filename = self.job_name + '.lock'
filename = self.job_code + '.lock'
return os.path.join(path, filename)
77 changes: 65 additions & 12 deletions django_cron/core.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import traceback
import time
import sys

from django.conf import settings
from django.utils.timezone import now as utc_now
from django.db.models import Q

from subprocess import check_output
from django_cron.helpers import get_class, get_current_time

from django_common.helper import send_mail


DEFAULT_LOCK_BACKEND = 'django_cron.backends.lock.cache.CacheLock'
DJANGO_CRON_OUTPUT_ERRORS = False
logger = logging.getLogger('django_cron')
Expand Down Expand Up @@ -48,12 +51,29 @@ class CronJobBase(object):
Following functions:
+ do - This is the actual business logic to be run at the given schedule
"""

SEND_FAILED_EMAIL = []
remove_successful_cron_logs = False

def __init__(self):
self.prev_success_cron = None

@classmethod
def get_code(self):
try:
if self.APPEND_IP_TO_CODE:
myserverip = None
try:
myserverip = check_output(['/usr/bin/ec2metadata', '--public-ipv4'])
myserverip = myserverip.strip()
except:
pass
if not myserverip:
myserverip = check_output(['hostname', '-I'])
myserverip = myserverip.strip()
return self.code + '-' + myserverip.decode('utf-8')
except:
return self.code

def set_prev_success_cron(self, prev_success_cron):
self.prev_success_cron = prev_success_cron

Expand Down Expand Up @@ -93,6 +113,9 @@ def __init__(self, cron_job_class, silent=False, dry_run=False, stdout=None):
self.write_log = getattr(
settings, 'DJANGO_CRON_OUTPUT_ERRORS', DJANGO_CRON_OUTPUT_ERRORS
)
self.send_error_email = getattr(settings,
'DJANGO_CRON_SEND_IMMEDIATE_ERROR_EMAIL',
False)

def should_run_now(self, force=False):
from django_cron.models import CronJobLog
Expand All @@ -109,19 +132,19 @@ def should_run_now(self, force=False):
return True

if cron_job.schedule.run_monthly_on_days is not None:
if not datetime.today().day in cron_job.schedule.run_monthly_on_days:
if not get_current_time().day in cron_job.schedule.run_monthly_on_days:
return False

if cron_job.schedule.run_weekly_on_days is not None:
if not datetime.today().weekday() in cron_job.schedule.run_weekly_on_days:
if not get_current_time().weekday() in cron_job.schedule.run_weekly_on_days:
return False

if cron_job.schedule.retry_after_failure_mins:
# We check last job - success or not
last_job = (
CronJobLog.objects.filter(code=cron_job.code)
CronJobLog.objects.filter(code=cron_job.get_code())
.order_by('-start_time')
.exclude(start_time__gt=datetime.today())
.exclude(start_time__gt=get_current_time())
.first()
)
if (
Expand All @@ -136,8 +159,8 @@ def should_run_now(self, force=False):
if cron_job.schedule.run_every_mins is not None:
try:
self.previously_ran_successful_cron = CronJobLog.objects.filter(
code=cron_job.code, is_success=True
).exclude(start_time__gt=datetime.today()).latest('start_time')
code=cron_job.get_code(), is_success=True
).exclude(start_time__gt=get_current_time()).latest('start_time')
except CronJobLog.DoesNotExist:
pass

Expand All @@ -158,7 +181,7 @@ def should_run_now(self, force=False):
actual_time = time.strptime("%s:%s" % (now.hour, now.minute), "%H:%M")
if actual_time >= user_time:
qset = CronJobLog.objects.filter(
code=cron_job.code, ran_at_time=time_data, is_success=True
code=cron_job.get_code(), ran_at_time=time_data, is_success=True
).filter(
Q(start_time__gt=now)
| Q(
Expand All @@ -177,7 +200,7 @@ def make_log(self, *messages, **kwargs):
cron_log = self.cron_log

cron_job = getattr(self, 'cron_job', self.cron_job_class)
cron_log.code = cron_job.code
cron_log.code = cron_job.get_code()

cron_log.is_success = kwargs.get('success', True)
cron_log.message = self.make_log_msg(messages)
Expand All @@ -187,6 +210,36 @@ def make_log(self, *messages, **kwargs):

if not cron_log.is_success and self.write_log:
logger.error("%s cronjob error:\n%s" % (cron_log.code, cron_log.message))

if self.send_error_email and not cron_log.is_success:
from django_cron.models import CronJobLog
try:
emails = [admin[1] for admin in settings.ADMINS]
if getattr(cron_job, "SEND_FAILED_EMAIL", []):
emails.extend(cron_job.SEND_FAILED_EMAIL)

failed_runs_cronjob_email_prefix = getattr(settings, 'FAILED_RUNS_CRONJOB_EMAIL_PREFIX', '')
min_failures = getattr(cron_job, 'MIN_NUM_FAILURES', 10)
if not min_failures:
min_failures = 10

last_min_cron_status = list(CronJobLog.objects.using("default").filter(
code=cron_log.code).order_by("-end_time").values_list("is_success", flat=True)[:min_failures])

#All of them should be failed ie false. Then only we have to send email
# Send on 3 failures. [True, False, False] ie [success, failed, failed] does not trigger email
if not any(last_min_cron_status):
send_mail(
'%s%s failed %s times in a row!' % (
failed_runs_cronjob_email_prefix,
cron_log.code,
min_failures,
),
cron_log.message,
settings.DEFAULT_FROM_EMAIL, emails
)
except Exception as e:
logger.exception(e)

def make_log_msg(self, messages):
full_message = ''
Expand Down Expand Up @@ -260,7 +313,7 @@ def run(self, force=False):
logger.debug(
"Running cron: %s code %s",
cron_job_class.__name__,
self.cron_job.code,
self.cron_job.get_code(),
)
self.make_log('Job in progress', success=True)
self.msg = self.cron_job.do()
Expand Down
2 changes: 1 addition & 1 deletion django_cron/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def do(self):
for cron in crons_to_check:

min_failures = getattr(cron, 'MIN_NUM_FAILURES', 10)
jobs = CronJobLog.objects.filter(code=cron.code).order_by('-end_time')[
jobs = CronJobLog.objects.filter(code=cron.get_code()).order_by('-end_time')[
:min_failures
]
failures = 0
Expand Down
29 changes: 25 additions & 4 deletions django_cron/management/commands/runcrons.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.core.management.base import BaseCommand
from django.conf import settings
from django.db import close_old_connections
from django_common.helper import send_mail

from django_cron import CronJobManager, get_class, get_current_time
from django_cron.models import CronJobLog
Expand All @@ -25,17 +26,28 @@ def add_arguments(self, parser):
action='store_true',
help="Just show what crons would be run; don't actually run them",
)
parser.add_argument(
'--run_class_list_name',
nargs='?',
help='Runs all the crons in the specified class list from settings. This is to override CRON_CLASSES hardcoding'
)

def handle(self, *args, **options):
"""
Iterates over all the CRON_CLASSES (or if passed in as a commandline argument)
and runs them.
"""

if not options['silent']:
self.stdout.write("Running Crons\n")
self.stdout.write("{0}\n".format("=" * 40))

cron_classes = options['cron_classes']

if options['run_class_list_name']:
list_name = options['run_class_list_name']
cron_classes = getattr(settings, list_name, [])

if cron_classes:
cron_class_names = cron_classes
else:
Expand All @@ -44,11 +56,20 @@ def handle(self, *args, **options):
try:
crons_to_run = [get_class(x) for x in cron_class_names]
except ImportError:
# Send an email to admin when the module load fails
error = traceback.format_exc()
self.stdout.write(
'ERROR: Make sure these are valid cron class names: %s\n\n%s'
% (cron_class_names, error)
)
self.stdout.write('ERROR: Make sure these are valid cron class names: %s\n\n%s' % (cron_class_names, error))
try:
emails = [admin[1] for admin in settings.ADMINS]
failed_runs_cronjob_email_prefix = getattr(settings, 'FAILED_RUNS_CRONJOB_EMAIL_PREFIX', '')
send_mail(
"URGENT!!! {} Error while importing crons".format(failed_runs_cronjob_email_prefix),
error,
settings.DEFAULT_FROM_EMAIL, emails
)
except Exception as e:
self.stdout.write(
'ERROR: While sending email: %s' % (e))
return

for cron_class in crons_to_run:
Expand Down
10 changes: 7 additions & 3 deletions django_cron/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ class CronJobLog(models.Model):

# This field is used to mark jobs executed in exact time.
# Jobs that run every X minutes, have this field empty.
ran_at_time = models.TimeField(null=True, blank=True, db_index=True, editable=False)
ran_at_time = models.TimeField(null=True, blank=True, db_index=True, editable=True)

def __unicode__(self):
return '%s (%s)' % (self.code, 'Success' if self.is_success else 'Fail')

def __str__(self):
return "%s (%s)" % (self.code, "Success" if self.is_success else "Fail")
return '%s (%s)' % (self.code, 'Success' if self.is_success else 'Fail')


class Meta:
index_together = [
Expand All @@ -38,3 +39,6 @@ class Meta:
class CronJobLock(models.Model):
job_name = models.CharField(max_length=200, unique=True)
locked = models.BooleanField(default=False)

def __str__(self):
return '%s (%s)' % (self.job_name, self.locked)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@

setup(
name='django-cron',
version='0.6.0',
version='0.6.3',
author='Sumit Chachra',
author_email='[email protected]',
url='http://github.com/tivix/django-cron',
url='https://github.com/Trendlyne-technologies/django-cron',
description='Running python crons in a Django project',
packages=find_packages(),
long_description=long_description,
Expand Down