Skip to content

Commit 0d1b0ba

Browse files
authored
Merge pull request Tivix#102 from HousekeepLtd/dry-run
Add cron feedback and dry-run functionality
2 parents 3493e1f + eeed67a commit 0d1b0ba

File tree

6 files changed

+188
-54
lines changed

6 files changed

+188
-54
lines changed

django_cron/__init__.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import timedelta
33
import traceback
44
import time
5+
import sys
56

67
from django.conf import settings
78
from django.utils.timezone import now as utc_now, localtime, is_naive
@@ -12,13 +13,21 @@
1213
logger = logging.getLogger('django_cron')
1314

1415

16+
class BadCronJobError(AssertionError):
17+
pass
18+
19+
1520
def get_class(kls):
1621
"""
1722
TODO: move to django-common app.
1823
Converts a string to a class.
1924
Courtesy: http://stackoverflow.com/questions/452969/does-python-have-an-equivalent-to-java-class-forname/452981#452981
2025
"""
2126
parts = kls.split('.')
27+
28+
if len(parts) == 1:
29+
raise ImportError("'{0}'' is not a valid import path".format(kls))
30+
2231
module = ".".join(parts[:-1])
2332
m = __import__(module)
2433
for comp in parts[1:]:
@@ -77,12 +86,11 @@ class CronJobManager(object):
7786
Used as a context manager via 'with' statement to ensure
7887
proper logger in cases of job failure.
7988
"""
80-
81-
def __init__(self, cron_job_class, silent=False, *args, **kwargs):
82-
super(CronJobManager, self).__init__(*args, **kwargs)
83-
89+
def __init__(self, cron_job_class, silent=False, dry_run=False, stdout=None):
8490
self.cron_job_class = cron_job_class
8591
self.silent = silent
92+
self.dry_run = dry_run
93+
self.stdout = stdout or sys.stdout
8694
self.lock_class = self.get_lock_class()
8795
self.previously_ran_successful_cron = None
8896

@@ -92,7 +100,6 @@ def should_run_now(self, force=False):
92100
"""
93101
Returns a boolean determining whether this cron should run now or not!
94102
"""
95-
96103
self.user_time = None
97104
self.previously_ran_successful_cron = None
98105

@@ -170,11 +177,20 @@ def __enter__(self):
170177
return self
171178

172179
def __exit__(self, ex_type, ex_value, ex_traceback):
173-
if ex_type == self.lock_class.LockFailedException:
180+
if ex_type is None:
181+
return True
182+
183+
non_logging_exceptions = [
184+
BadCronJobError, self.lock_class.LockFailedException
185+
]
186+
187+
if ex_type in non_logging_exceptions:
174188
if not self.silent:
189+
self.stdout.write("{0}\n".format(ex_value))
175190
logger.info(ex_value)
176-
177-
elif ex_type is not None:
191+
else:
192+
if not self.silent:
193+
self.stdout.write(u"[\N{HEAVY BALLOT X}] {0}\n".format(self.cron_job_class.code))
178194
try:
179195
trace = "".join(traceback.format_exception(ex_type, ex_value, ex_traceback))
180196
self.make_log(self.msg, trace, success=False)
@@ -189,17 +205,29 @@ def run(self, force=False):
189205
apply the logic of the schedule and call do() on the CronJobBase class
190206
"""
191207
cron_job_class = self.cron_job_class
208+
192209
if not issubclass(cron_job_class, CronJobBase):
193-
raise Exception('The cron_job to be run must be a subclass of %s' % CronJobBase.__name__)
210+
raise BadCronJobError('The cron_job to be run must be a subclass of %s' % CronJobBase.__name__)
211+
212+
if not hasattr(cron_job_class, 'code'):
213+
raise BadCronJobError(
214+
"Cron class '{0}' does not have a code attribute"
215+
.format(cron_job_class.__name__)
216+
)
194217

195218
with self.lock_class(cron_job_class, self.silent):
196219
self.cron_job = cron_job_class()
197220

198221
if self.should_run_now(force):
199-
logger.debug("Running cron: %s code %s", cron_job_class.__name__, self.cron_job.code)
200-
self.msg = self.cron_job.do()
201-
self.make_log(self.msg, success=True)
202-
self.cron_job.set_prev_success_cron(self.previously_ran_successful_cron)
222+
if not self.dry_run:
223+
logger.debug("Running cron: %s code %s", cron_job_class.__name__, self.cron_job.code)
224+
self.msg = self.cron_job.do()
225+
self.make_log(self.msg, success=True)
226+
self.cron_job.set_prev_success_cron(self.previously_ran_successful_cron)
227+
if not self.silent:
228+
self.stdout.write(u"[\N{HEAVY CHECK MARK}] {0}\n".format(self.cron_job.code))
229+
elif not self.silent:
230+
self.stdout.write(u"[ ] {0}\n".format(self.cron_job.code))
203231

204232
def get_lock_class(self):
205233
name = getattr(settings, 'DJANGO_CRON_LOCK_BACKEND', DEFAULT_LOCK_BACKEND)

django_cron/management/commands/runcrons.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import print_function
12
import traceback
23
from datetime import timedelta
34

@@ -28,41 +29,60 @@ def add_arguments(self, parser):
2829
action='store_true',
2930
help='Do not push any message on console'
3031
)
32+
parser.add_argument(
33+
'--dry-run',
34+
action='store_true',
35+
help="Just show what crons would be run; don't actually run them"
36+
)
3137

3238
def handle(self, *args, **options):
3339
"""
3440
Iterates over all the CRON_CLASSES (or if passed in as a commandline argument)
3541
and runs them.
3642
"""
43+
if not options['silent']:
44+
self.stdout.write("Running Crons\n")
45+
self.stdout.write("{0}\n".format("=" * 40))
46+
3747
cron_classes = options['cron_classes']
3848
if cron_classes:
3949
cron_class_names = cron_classes
4050
else:
4151
cron_class_names = getattr(settings, 'CRON_CLASSES', [])
4252

43-
crons_to_run = [get_class(x) for x in cron_class_names]
53+
try:
54+
crons_to_run = [get_class(x) for x in cron_class_names]
55+
except ImportError:
56+
error = traceback.format_exc()
57+
self.stdout.write('ERROR: Make sure these are valid cron class names: %s\n\n%s' % (cron_class_names, error))
58+
return
4459

4560
for cron_class in crons_to_run:
4661
run_cron_with_cache_check(
4762
cron_class,
4863
force=options['force'],
49-
silent=options['silent']
64+
silent=options['silent'],
65+
dry_run=options['dry_run'],
66+
stdout=self.stdout
5067
)
5168

5269
clear_old_log_entries()
5370
close_old_connections()
5471

5572

56-
def run_cron_with_cache_check(cron_class, force=False, silent=False):
73+
def run_cron_with_cache_check(
74+
cron_class, force=False, silent=False, dry_run=False, stdout=None
75+
):
5776
"""
5877
Checks the cache and runs the cron or not.
5978
6079
@cron_class - cron class to run.
6180
@force - run job even if not scheduled
6281
@silent - suppress notifications
82+
@dryrun - don't actually perform the cron job
83+
@stdout - where to write feedback to
6384
"""
64-
65-
with CronJobManager(cron_class, silent) as manager:
85+
with CronJobManager(cron_class, silent=silent, dry_run=dry_run, stdout=stdout) as manager:
6686
manager.run(force)
6787

6888

0 commit comments

Comments
 (0)