├── django_cron ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── cron_reset.py │ │ ├── cron_requeue.py │ │ └── cronjobs.py ├── south_migrations │ └── __init__.py ├── admin_urls.py ├── cron_settings.py ├── admin_views.py ├── migrations │ ├── 0002_auto_20161118_1325.py │ ├── __init__.py │ └── 0001_initial.py ├── templates │ └── django_cron │ │ └── fixed_job_stuck.txt ├── signals.py ├── models.py ├── __init__.py └── base.py ├── .gitignore ├── setup.py ├── LICENCE.txt └── README.rst /django_cron/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_cron/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_cron/south_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | *.egg-info 4 | dist 5 | .tox 6 | .coverage 7 | htmlcov 8 | -------------------------------------------------------------------------------- /django_cron/admin_urls.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.conf.urls.defaults import url, patterns 3 | except ImportError: 4 | from django.conf.urls import url, patterns 5 | 6 | urlpatterns = patterns( 7 | 'django_cron.admin_views', 8 | url(r'^restart/$', 'restart'), 9 | ) 10 | -------------------------------------------------------------------------------- /django_cron/cron_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | 4 | 5 | base_dir = getattr(settings, 'BASE_DIR', '') or getattr(settings, 'PROJECT_DIR', '') 6 | PID_FILE = getattr(settings, 'CRON_PID_FILE', os.path.join(base_dir, 'cron_pid.pid')) 7 | RETRY = getattr(settings, 'CRON_RETRY', False) 8 | SINGLE_JOB_MODE = getattr(settings, 'CRON_SINGLE_JOB_MODE', False) 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name="django-cron", 4 | version="0.1", 5 | description="Django application automating tasks.", 6 | author="Reavis Sutphin-Gray", 7 | author_email="reavis-django-cron@sutphin-gray.com", 8 | packages=find_packages(), 9 | include_package_data=True, 10 | ) -------------------------------------------------------------------------------- /django_cron/admin_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.views.decorators import staff_member_required 2 | from django.core.urlresolvers import reverse 3 | from django.http import HttpResponseRedirect 4 | 5 | 6 | from models import Job 7 | 8 | 9 | @staff_member_required 10 | def restart(request): 11 | for j in Job.objects.all(): 12 | j.queued = True 13 | j.save() 14 | return HttpResponseRedirect('%s' % reverse('admin:index')) 15 | -------------------------------------------------------------------------------- /django_cron/migrations/0002_auto_20161118_1325.py: -------------------------------------------------------------------------------- 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', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='job', 16 | name='last_run', 17 | field=models.DateTimeField(default=None, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_cron/templates/django_cron/fixed_job_stuck.txt: -------------------------------------------------------------------------------- 1 | django_cron was stuck as we found process #{{ pid }} is not running, yet status.executing==True. 2 | 3 | host: {{ host }} 4 | site: {{ settings.SITE_DOMAIN }} 5 | project path: {% firstof settings.BASE_DIR settings.PROJECT_DIR %} 6 | 7 | {% if non_queued_jobs.exists %} 8 | The following cron jobs are not queued 9 | {% for job in non_queued_jobs %} 10 | Name: {{ job.name }} 11 | Last run: {{ job.last_run }} 12 | Queued: {{ job.queued }} 13 | {% endfor %} 14 | 15 | {%else %} 16 | All cron jobs are queued. 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /django_cron/management/commands/cron_reset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run the cron service (intended to be executed from a cron job) 3 | 4 | usage: manage.py cronjobs 5 | """ 6 | 7 | from django.core.management.base import NoArgsCommand 8 | from django_cron.models import Cron 9 | 10 | 11 | class Command(NoArgsCommand): 12 | help = "reset the django cron status" 13 | 14 | def handle_noargs(self, **options): 15 | print 'Django Cron status reset' 16 | status, created = Cron.objects.get_or_create(pk=1) 17 | status.executing = False 18 | status.save() 19 | print 'Done' -------------------------------------------------------------------------------- /django_cron/management/commands/cron_requeue.py: -------------------------------------------------------------------------------- 1 | """ 2 | Requeue all the cronjobs. 3 | 4 | Usage: ./manage.py cron_requeue 5 | """ 6 | 7 | from django.core.management.base import NoArgsCommand 8 | from django_cron.models import Job 9 | 10 | 11 | class Command(NoArgsCommand): 12 | help = "Set all jobs to be queued again." 13 | 14 | def handle_noargs(self, **options): 15 | print 'Django Cron requeueing all jobs.' 16 | for job in Job.objects.all(): 17 | job.queued = True 18 | job.save() 19 | print "Requeued ", job 20 | print 'Done' 21 | -------------------------------------------------------------------------------- /django_cron/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django 1.7+ migrations for django_cron app 3 | 4 | This package does not contain South migrations. 5 | 6 | """ 7 | 8 | SOUTH_ERROR_MESSAGE = """\n 9 | For South support, customize the SOUTH_MIGRATION_MODULES setting like so: 10 | 11 | SOUTH_MIGRATION_MODULES = { 12 | 'django_cron': 'django_cron.south_migrations', 13 | } 14 | """ 15 | 16 | # Ensure the user is not using Django 1.6 or below with South 17 | try: 18 | from django.db import migrations # noqa 19 | except ImportError: 20 | from django.core.exceptions import ImproperlyConfigured 21 | raise ImproperlyConfigured(SOUTH_ERROR_MESSAGE) -------------------------------------------------------------------------------- /django_cron/management/commands/cronjobs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run the cron service (intended to be executed from a cron job) 3 | 4 | Rsage: manage.py cronjobs 5 | """ 6 | 7 | import sys 8 | import signal 9 | from datetime import datetime 10 | 11 | from django.conf import settings 12 | from django.core.management.base import NoArgsCommand 13 | import django_cron 14 | 15 | 16 | # Exit when the command last longer than CRON_TIMEOUT, if it's set 17 | if getattr(settings, 'CRON_TIMEOUT', False): 18 | def timeout_check(signum, stack): 19 | print 'Timeout, exiting!' 20 | sys.exit() 21 | 22 | signal.signal(signal.SIGALRM, timeout_check) 23 | signal.alarm(settings.CRON_TIMEOUT) 24 | 25 | 26 | class Command(NoArgsCommand): 27 | help = "run the cron services (intended to be executed from a cron job)" 28 | 29 | def handle_noargs(self, **options): 30 | django_cron.autodiscover(start_timer=False, registering=False) 31 | print "%s: Cronjobs for %s finished" % (datetime.now(), settings.SITE_NAME) 32 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Dj Gilcrease 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /django_cron/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2007-2008, Dj Gilcrease 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | """ 23 | from django.dispatch import Signal 24 | 25 | cron_queued = Signal() 26 | cron_done = Signal(providing_args=["job"]) -------------------------------------------------------------------------------- /django_cron/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-07 14:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Cron', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('executing', models.BooleanField(default=False)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Job', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=100)), 29 | ('run_frequency', models.PositiveIntegerField(default=1440)), 30 | ('last_run', models.DateTimeField(default=django.utils.timezone.now)), 31 | ('instance', models.TextField()), 32 | ('args', models.TextField()), 33 | ('kwargs', models.TextField()), 34 | ('queued', models.BooleanField(default=True)), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /django_cron/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2007-2008, Dj Gilcrease 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | """ 23 | from django.db import models 24 | try: 25 | from django.utils import timezone 26 | now = timezone.now 27 | except ImportError: 28 | # Django<=1.3 compatibility 29 | from datetime import datetime 30 | now = datetime.now 31 | 32 | 33 | class Job(models.Model): 34 | 35 | name = models.CharField(max_length=100) 36 | 37 | # Time between job runs (in minutes) // default: 1 day 38 | run_frequency = models.PositiveIntegerField(default=1440) 39 | last_run = models.DateTimeField(default=None, null=True) 40 | 41 | instance = models.TextField() 42 | args = models.TextField() 43 | kwargs = models.TextField() 44 | queued = models.BooleanField(default=True) 45 | 46 | def __unicode__(self): 47 | return self.name 48 | 49 | 50 | class Cron(models.Model): 51 | executing = models.BooleanField(default=False) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | How to install django-cron 2 | ========================== 3 | 4 | 1. Put ``django_cron`` into your python path 5 | 2. Add ``django_cron`` to ``INSTALLED_APPS`` in your ``settings.py`` file 6 | 3. Add the following code to the beginning of your ``urls.py`` file (just after the imports): 7 | 8 | .. code-block:: python 9 | 10 | import django_cron 11 | django_cron.autodiscover() 12 | 13 | 4. Create a file called ``cron.py`` inside each installed app that you want to add a recurring job to. The app must be installed via the ``INSTALLED_APPS`` in your ``settings.py`` or the autodiscover will not find it. 14 | 15 | Example `cron.py`: 16 | 17 | .. code-block:: python 18 | 19 | from django_cron import cronScheduler, Job, HOUR, DAY, WEEK, MONTH 20 | 21 | # This is a function I wrote to check a feedback email address 22 | # and add it to our database. Replace with your own imports 23 | from MyMailFunctions import check_feedback_mailbox 24 | 25 | class CheckMail(Job): 26 | """ 27 | Cron Job that checks the lgr users mailbox and adds any 28 | approved senders' attachments to the db 29 | """ 30 | 31 | # run every hours 32 | run_every = HOUR 33 | 34 | def job(self): 35 | # This will be executed every hour 36 | check_feedback_mailbox() 37 | 38 | cronScheduler.register(CheckMail) 39 | 40 | Notes on andybak's fork 41 | ----------------------- 42 | 43 | - Feature: Run via normal system cron using a management command rather than the original request based triggering. Makes it usable on low-traffic sites where there might not be enough requests to reliably run your cron jobs. 44 | - Feature: Changed time units for run_every to be minutes rather than seconds. This makes the numbers a bit less unwieldy and I couldn't see a need for the higher resolution 45 | - Feature: Added some convenient time constants: HOUR, DAY, WEEK, MONTH (last one is actually 52/12 weeks) 46 | - Feature: Email notifications of exceptions while running a job. 47 | - Feature: Admin messages to all staff when jobs fail. 48 | - Feature: Allow restarting jobs via a GET to [admin_url]/cron/restart/ (I know... I know... GET's shouldn't do this...) 49 | - Feature: Support notifications and restarting jobs via the app django-admin-notifications if jobs have missed their schedule. 50 | - Fixed: Registering jobs used to also run them - this tends to block the dev server if there are any long processes like a backup. Added a argument 'registering=False' to the method execute(). 51 | 52 | Extra installation steps 53 | ~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | 1. Add an entry to your crontab. I use: 56 | 57 | .. code-block:: bash 58 | 59 | \*/1 \* \* \* \* /path/to/python /path/to/manage.py cronjobs >> $HOME/cron.log 2>>cron_error.log 60 | 61 | (If you do ``PYTHONPATH`` wrangling in your ``.bash_profile`` then this might need moving into your manage.py or a similar wrapper script) 62 | 63 | 2. Add this to your root urls if you want to support restarting cron jobs via a GET: 64 | 65 | .. code-block:: python 66 | 67 | url(r'^admin/cron/', include('django_cron.admin_urls')), 68 | -------------------------------------------------------------------------------- /django_cron/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2007-2008, Dj Gilcrease 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | """ 23 | from base import Job, cronScheduler, HOUR, DAY, WEEK, MONTH # noqa 24 | 25 | 26 | def autodiscover(start_timer=True, registering=True): 27 | """ 28 | Auto-discover INSTALLED_APPS cron.py modules and fail silently when 29 | not present. This forces an import on them to register any cron jobs they 30 | may want. 31 | """ 32 | import imp 33 | from django.conf import settings 34 | 35 | for app in settings.INSTALLED_APPS: 36 | # For each app, we need to look for an cron.py inside that app's 37 | # package. We can't use os.path here -- recall that modules may be 38 | # imported different ways (think zip files) -- so we need to get 39 | # the app's __path__ and look for cron.py on that path. 40 | 41 | # Step 1: find out the app's __path__ Import errors here will (and 42 | # should) bubble up, but a missing __path__ (which is legal, but weird) 43 | # fails silently -- apps that do weird things with __path__ might 44 | # need to roll their own cron registration. 45 | try: 46 | app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__ 47 | except AttributeError: 48 | continue 49 | 50 | # Step 2: use imp.find_module to find the app's admin.py. For some 51 | # reason imp.find_module raises ImportError if the app can't be found 52 | # but doesn't actually try to import the module. So skip this app if 53 | # its admin.py doesn't exist 54 | try: 55 | imp.find_module('cron', app_path) 56 | except ImportError: 57 | continue 58 | 59 | # Step 3: import the app's cron file. If this has errors we want them 60 | # to bubble up. 61 | __import__("%s.cron" % app) 62 | 63 | # Step 4: once we find all the cron jobs, start the cronScheduler 64 | cronScheduler.execute(start_timer=start_timer, registering=registering) 65 | -------------------------------------------------------------------------------- /django_cron/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2007-2008, Dj Gilcrease 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | """ 23 | import logging 24 | import os 25 | import cPickle 26 | from threading import Timer 27 | from datetime import datetime 28 | from datetime import timedelta 29 | import sys 30 | import socket 31 | try: 32 | from django.utils import timezone 33 | except ImportError: 34 | timezone = None 35 | 36 | try: 37 | from django.db import ProgrammingError 38 | except ImportError: 39 | # make it compatible with older version of django 40 | ProgrammingError = Exception 41 | from django.template.loader import render_to_string 42 | from django.conf import settings 43 | from django.core.mail import mail_admins 44 | from signals import cron_done 45 | import cron_settings 46 | 47 | 48 | HOUR = 60 49 | DAY = HOUR * 24 50 | WEEK = DAY * 7 51 | MONTH = int(WEEK * 4.333) # well sorta 52 | 53 | # How often to check if jobs are ready to be run (in seconds) 54 | # in reality if you have a multithreaded server, it may get checked 55 | # more often that this number suggests, so keep an eye on it... 56 | # default value: 300 seconds == 5 min 57 | polling_frequency = getattr(settings, "CRON_POLLING_FREQUENCY", 300) 58 | 59 | cron_pid_file = cron_settings.PID_FILE 60 | 61 | 62 | def get_now(): 63 | if timezone and settings.USE_TZ: 64 | now = timezone.now() 65 | else: 66 | now = datetime.now() 67 | # Discard the seconds to prevent drift. Thanks to Josh Cartmell 68 | now = datetime(now.year, now.month, now.day, now.hour, now.minute) 69 | return now 70 | 71 | 72 | class Job(object): 73 | 74 | run_every = DAY 75 | unreliable = 0 # for unreliable jobs, setting is in minutes 76 | 77 | def run(self, *args, **kwargs): 78 | self.job() 79 | cron_done.send(sender=self, *args, **kwargs) 80 | 81 | def job(self): 82 | """ 83 | Should be overridden (this way is cleaner, but the old way - overriding run() - will still work) 84 | """ 85 | pass 86 | 87 | 88 | class CronScheduler(object): 89 | 90 | def register(self, job_class, *args, **kwargs): 91 | """ 92 | Register the given Job with the scheduler class 93 | """ 94 | 95 | # Move import here to silence Django 1.8 warnings about importing models early 96 | import models 97 | 98 | job_instance = job_class() 99 | 100 | if not isinstance(job_instance, Job): 101 | raise TypeError("You can only register a Job not a %r" % job_class) 102 | 103 | try: 104 | job, created = models.Job.objects.get_or_create(name=str(job_instance.__class__)) 105 | if created: 106 | job.instance = cPickle.dumps(job_instance) 107 | job.args = cPickle.dumps(args) 108 | job.kwargs = cPickle.dumps(kwargs) 109 | job.run_frequency = job_instance.run_every 110 | job.save() 111 | except ProgrammingError as e: 112 | # Any managemment commands that run before the database tables have been created 113 | # will trigger an exception. Convert this into a warning. 114 | if "django_cron_job' doesn't exist" not in str(e): 115 | raise 116 | else: 117 | logging.warn("Cron jobs not registered as tables don't yet exist") 118 | 119 | def unregister(self, job_class, *args, **kwargs): 120 | 121 | # Move import here to silence Django 1.8 warnings about importing models early 122 | import models 123 | 124 | job_instance = job_class() 125 | if not isinstance(job_instance, Job): 126 | raise TypeError("You can only unregister a Job not a %r" % job_class) 127 | try: 128 | job = models.Job.objects.get(name=str(job_instance.__class__)) 129 | job.delete() 130 | except models.Job.DoesNotExist: 131 | pass 132 | 133 | def execute(self, start_timer=True, registering=False): 134 | """ 135 | Queue all Jobs for execution 136 | """ 137 | 138 | # Move import here to silence Django 1.8 warnings about importing models early 139 | import models 140 | 141 | if not registering: 142 | status, created = models.Cron.objects.get_or_create(pk=1) 143 | 144 | ###PID code 145 | if cron_pid_file: 146 | if not os.path.exists(cron_pid_file): 147 | f = open(cron_pid_file, 'w') #create the file if it doesn't exist yet. 148 | f.close() 149 | 150 | # This is important for 2 reasons: 151 | # 1. It keeps us for running more than one instance of the 152 | # same job at a time 153 | # 2. It reduces the number of polling threads because they 154 | # get killed off if they happen to check while another 155 | # one is already executing a job (only occurs with 156 | # multi-threaded servers) 157 | 158 | if status.executing: 159 | print "Already executing" 160 | ###PID code 161 | ###check if django_cron is stuck 162 | if cron_pid_file: 163 | pid_file = open(cron_pid_file, 'r') 164 | pid_content = pid_file.read() 165 | pid_file.close() 166 | if not pid_content: 167 | pass#File is empty, do nothing 168 | else: 169 | pid = int(pid_content) 170 | if os.path.exists('/proc/%s' % pid): 171 | print 'Verified! Process with pid %s is running.' % pid 172 | else: 173 | print 'Oops! process with pid %s is not running.' % pid 174 | print 'Fixing status in db. ' 175 | status.executing = False 176 | status.save() 177 | subject = 'Fixed cron job for %s' % settings.SITE_NAME 178 | context = { 179 | 'pid': pid, 180 | 'host': socket.gethostname(), 181 | 'settings': settings, 182 | 'non_queued_jobs': models.Job.objects.filter(queued=False), 183 | 'queued_jobs': models.Job.objects.exclude(queued=False), 184 | } 185 | body = render_to_string('django_cron/fixed_job_stuck.txt', context) 186 | mail_admins(subject, body, fail_silently=True) 187 | return 188 | 189 | status.executing = True 190 | ###PID code 191 | if cron_pid_file: 192 | pid_file = open(cron_pid_file, 'w') 193 | pid_file.write(str(os.getpid())) 194 | pid_file.close() 195 | try: 196 | status.save() 197 | except: 198 | # this will fail if you're debugging, so we want it 199 | # to fail silently and start the timer again so we 200 | # can pick up where we left off once debugging is done 201 | if start_timer: 202 | # Set up for this function to run again 203 | Timer(polling_frequency, self.execute).start() 204 | return 205 | 206 | jobs = models.Job.objects.filter(queued=True) 207 | if jobs and cron_settings.SINGLE_JOB_MODE: 208 | jobs = [jobs[0]] # When SINGLE_JOB_MODE is on we only run one job at a time 209 | 210 | for job in jobs: 211 | 212 | now = get_now() 213 | if job.last_run: 214 | last_run = datetime(job.last_run.year, job.last_run.month, job.last_run.day, job.last_run.hour, job.last_run.minute) 215 | else: 216 | last_run = datetime(2000,1,1) # A long time ago. 217 | since_last_run = now - last_run 218 | inst = None 219 | 220 | if since_last_run >= timedelta(minutes=job.run_frequency): 221 | try: 222 | try: 223 | inst = cPickle.loads(str(job.instance)) 224 | args = cPickle.loads(str(job.args)) 225 | kwargs = cPickle.loads(str(job.kwargs)) 226 | except AttributeError, e: 227 | if e.message.startswith(''''module' object has no attribute'''): 228 | job.delete() # The job had been deleted in code 229 | raise 230 | except ImportError, e: 231 | if 'No module' in e.message: 232 | job.delete() # The whole cron.py file was deleted 233 | raise 234 | 235 | run_job_with_retry = None 236 | 237 | def run_job(): 238 | inst.run(*args, **kwargs) 239 | job.last_run = datetime.now() 240 | job.save() 241 | 242 | if cron_settings.RETRY: 243 | try: 244 | # When retrying library is available we retry a few times 245 | from retrying import retry 246 | @retry( 247 | wait_exponential_multiplier=1000, 248 | wait_exponential_max=10000, 249 | stop_max_delay=30000 250 | ) 251 | def run_job_with_retry(): 252 | run_job() 253 | except ImportError: 254 | pass 255 | 256 | if run_job_with_retry: 257 | run_job_with_retry() 258 | else: 259 | run_job() 260 | 261 | except Exception, err: 262 | # If the job throws an error, just remove it from 263 | # the queue. That way we can find/fix the error and 264 | # requeue the job manually 265 | unreliable_time = timedelta(minutes=getattr(inst, 'unreliable', 0)) 266 | if job.id: # Job might have been deleted 267 | # when the job is marked unreliable, we fail silently if it's within unreliable time. 268 | if since_last_run <= unreliable_time: 269 | job.queued = False 270 | job.save() 271 | if since_last_run >= unreliable_time: 272 | # besides sending error emails, mark queued=False 273 | if job.id: 274 | job.queued = False 275 | job.save() 276 | import traceback 277 | exc_info = sys.exc_info() 278 | stack = ''.join(traceback.format_tb(exc_info[2])) 279 | if not getattr(settings, 'LOCAL_DEV', False): 280 | self.mail_exception(job.name, inst and inst.__module__, err, stack) 281 | else: 282 | print stack 283 | status.executing = False 284 | status.save() 285 | 286 | if start_timer: 287 | # Set up for this function to run again 288 | Timer(polling_frequency, self.execute).start() 289 | 290 | def mail_exception(self, job, module, err, stack=None): 291 | 292 | subject = 'Cron job failed for %s' % settings.SITE_NAME 293 | body = ''' 294 | Cron job failed for %s 295 | Job: %s 296 | Module: %s 297 | Error message: %s 298 | Time: %s 299 | 300 | Stack Trace: %s 301 | ''' % (settings.SITE_NAME, job, module, str(err), datetime.now().strftime("%d %b %Y"), ''.join(stack)) 302 | 303 | mail_admins(subject, body, fail_silently=True) 304 | 305 | 306 | cronScheduler = CronScheduler() 307 | 308 | 309 | --------------------------------------------------------------------------------