def save(self, *args, **kwargs): if not self.disabled: if self.pk: j = Job.objects.get(pk=self.pk) else: j = self if not self.next_run or j.params != self.params: logger.debug("Updating 'next_run") next_run = self.next_run if not next_run: next_run = dates.now() self.next_run = dates.make_aware(self.rrule.after(next_run)) else: self.next_run = None super(Job, self).save(*args, **kwargs)
def save(self, *args, **kwargs): if not self.disabled: if self.pk: j = Job.objects.get(pk=self.pk) else: j = self if not self.next_run or j.params != self.params: logger.debug("Updating 'next_run") next_run = self.next_run if not next_run: next_run = dates.now() next_run = dates.make_naive(next_run) self.next_run = dates.make_aware(self.rrule.after(next_run)) else: self.next_run = None super(Job, self).save(*args, **kwargs)
class Job(models.Model): """ A recurring ``django-admin`` command to be run. """ name = models.CharField(_("name"), max_length=200) frequency = models.CharField(_("frequency"), choices=freqs, max_length=10) params = models.TextField(_("params"), null=True, blank=True, help_text=_( 'Comma-separated list of ' '<a href="http://labix.org/python-dateutil" ' 'target="_blank">rrule parameters</a>. ' 'e.g: interval:15')) command = models.CharField(_("command"), max_length=200, blank=True, help_text=_("A valid django-admin command to " "run.")) args = models.CharField( _("args"), max_length=200, blank=True, help_text=_("Space separated list; e.g: arg1 option1=True")) disabled = models.BooleanField(default=False, help_text=_('If checked this ' 'job will never ' 'run.')) next_run = models.DateTimeField(_("next run"), blank=True, null=True, help_text=_( "If you don't set this it will" " be determined automatically")) last_run = models.DateTimeField(_("last run"), editable=False, blank=True, null=True) is_running = models.BooleanField(default=False, editable=False) last_run_successful = models.BooleanField(default=True, blank=False, null=False, editable=False) subscribers = models.ManyToManyField(User, blank=True, limit_choices_to={'is_staff': True}) lock_file = models.CharField(max_length=255, blank=True, editable=False) force_run = models.BooleanField(default=False) objects = JobManager() class Meta: ordering = ( 'disabled', 'next_run', ) def __unicode__(self): if self.disabled: return _(u"%(name)s - disabled") % {'name': self.name} return u"%s - %s" % (self.name, self.timeuntil) def save(self, *args, **kwargs): if not self.disabled: if self.pk: j = Job.objects.get(pk=self.pk) else: j = self if not self.next_run or j.params != self.params: logger.debug("Updating 'next_run") next_run = self.next_run if not next_run: next_run = dates.now() next_run = dates.make_naive(next_run) self.next_run = dates.make_aware(self.rrule.after(next_run)) else: self.next_run = None super(Job, self).save(*args, **kwargs) def get_timeuntil(self): """ Returns a string representing the time until the next time this Job will be run (actually, the "string" returned is really an instance of ``ugettext_lazy``). >>> from chronograph.compatibility.dates import now >>> job = Job(next_run=now()) >>> job.get_timeuntil().translate('en') u'due' """ if self.disabled: return _('never (disabled)') delta = self.next_run - dates.now() if delta.days < 0: # The job is past due and should be run as soon as possible if self.check_is_running(): return _('running') return _('due') elif delta.seconds < 60: # Adapted from django.utils.timesince count = lambda n: ungettext('second', 'seconds', n) return ugettext('%(number)d %(type)s') % { 'number': delta.seconds, 'type': count(delta.seconds) } return timeuntil(self.next_run) get_timeuntil.short_description = _('time until next run') timeuntil = property(get_timeuntil) def get_rrule(self): """ Returns the rrule objects for this ``Job``. Can also be accessed via the ``rrule`` property of the ``Job``. # Every minute >>> last_run = datetime(2011, 8, 4, 7, 19) >>> job = Job(frequency="MINUTELY", params="interval:1", last_run=last_run) >>> print job.get_rrule().after(last_run) 2011-08-04 07:20:00 # Every 2 hours >>> job = Job(frequency="HOURLY", params="interval:2", last_run=last_run) >>> print job.get_rrule().after(last_run) 2011-08-04 09:19:00 """ frequency = eval('rrule.%s' % self.frequency) return rrule.rrule(frequency, dtstart=self.last_run, **self.get_params()) rrule = property(get_rrule) def param_to_int(self, param_value): """ Converts a valid rrule parameter to an integer if it is not already one, else raises a ``ValueError``. The following are equivalent: >>> job = Job(params = "byweekday:1,2,4,5") >>> job.get_params() {'byweekday': [1, 2, 4, 5]} >>> job = Job(params = "byweekday:TU,WE,FR,SA") >>> job.get_params() {'byweekday': [1, 2, 4, 5]} """ if param_value in RRULE_WEEKDAY_DICT: return RRULE_WEEKDAY_DICT[param_value] try: val = int(param_value) except ValueError: raise ValueError('rrule parameter should be integer or weekday ' 'constant (e.g. MO, TU, etc.). ' 'Error on: %s' % param_value) else: return val def get_params(self): """ Converts a string of parameters into a dict. >>> job = Job(params = "count:1;bysecond:1;byminute:1,2,4,5") >>> job.get_params() {'count': 1, 'byminute': [1, 2, 4, 5], 'bysecond': 1} """ if self.params is None: return {} params = self.params.split(';') param_dict = [] for param in params: if param.strip() == "": continue # skip blanks param = param.split(':') if len(param) == 2: param = (str(param[0]).strip(), [ self.param_to_int(p.strip()) for p in param[1].split(',') ]) if len(param[1]) == 1: param = (param[0], param[1][0]) param_dict.append(param) return dict(param_dict) def get_args(self): """ Processes the args and returns a tuple or (args, options) for passing to ``call_command``. >>> job = Job(args="arg1 arg2 kwarg1='some value'") >>> job.get_args() (['arg1', 'arg2', "value'"], {'kwarg1': "'some"}) """ args = [] options = {} for arg in self.args.split(): if arg.find('=') > -1: key, value = arg.split('=') options[smart_str(key)] = smart_str(value) else: args.append(arg) return (args, options) def is_due(self): """ >>> from chronograph.compatibility.dates import now >>> job = Job(next_run=now()) >>> job.is_due() True >>> job = Job(next_run=now()+timedelta(seconds=60)) >>> job.is_due() False >>> job.force_run = True >>> job.is_due() True >>> job = Job(next_run=now(), disabled=True) >>> job.is_due() False """ reqs = (self.next_run <= dates.now() and self.disabled == False and self.check_is_running() == False) return (reqs or self.force_run) def run(self): """ Runs this ``Job``. A ``Log`` will be created if there is any output from either stdout or stderr. Returns ``True`` if the ``Job`` ran, ``False`` otherwise. """ if not self.disabled: if not self.check_is_running() and self.is_due(): call_command('run_job', str(self.pk)) return True return False def handle_run(self): """ This method implements the code to actually run a ``Job``. This is meant to be run, primarily, by the `run_job` management command as a subprocess, which can be invoked by calling this ``Job``\'s ``run`` method. """ args, options = self.get_args() stdout = StringIO() stderr = StringIO() # Redirect output so that we can log it if there is any ostdout = sys.stdout ostderr = sys.stderr sys.stdout = stdout sys.stderr = stderr stdout_str, stderr_str = "", "" heartbeat = JobHeartbeatThread() run_date = dates.now() self.is_running = True self.lock_file = heartbeat.lock_file.name self.save() heartbeat.start() try: logger.debug("Calling command '%s'" % self.command) call_command(self.command, *args, **options) logger.debug("Command '%s' completed" % self.command) self.last_run_successful = True except Exception, e: # The command failed to run; log the exception t = loader.get_template('chronograph/error_message.txt') c = Context({ 'exception': unicode(e), 'traceback': ['\n'.join(traceback.format_exception(*sys.exc_info()))] }) stderr_str += t.render(c) self.last_run_successful = False # Stop the heartbeat logger.debug("Stopping heartbeat") heartbeat.stop() heartbeat.join() duration = dates.total_seconds(dates.now() - run_date) self.is_running = False self.lock_file = "" # Only care about minute-level resolution self.last_run = dates.localtime( dates.make_aware( datetime(run_date.year, run_date.month, run_date.day, run_date.hour, run_date.minute))) # If this was a forced run, then don't update the # next_run date if self.force_run: logger.debug("Resetting 'force_run'") self.force_run = False else: logger.debug("Determining 'next_run'") while self.next_run < dates.now(): self.next_run = dates.make_aware( self.rrule.after(self.next_run)) logger.debug("'next_run = ' %s" % self.next_run) self.save() # If we got any output, save it to the log stdout_str += stdout.getvalue() stderr_str += stderr.getvalue() if stderr_str: # If anything was printed to stderr, consider the run # unsuccessful self.last_run_successful = False if stdout_str or stderr_str: log = Log.objects.create(job=self, run_date=run_date, stdout=stdout_str, stderr=stderr_str, success=self.last_run_successful, duration=duration) # Redirect output back to default sys.stdout = ostdout sys.stderr = ostderr
def handle_run(self): """ This method implements the code to actually run a ``Job``. This is meant to be run, primarily, by the `run_job` management command as a subprocess, which can be invoked by calling this ``Job``\'s ``run`` method. """ args, options = self.get_args() stdout = StringIO() stderr = StringIO() # Redirect output so that we can log it if there is any ostdout = sys.stdout ostderr = sys.stderr sys.stdout = stdout sys.stderr = stderr stdout_str, stderr_str = "", "" heartbeat = JobHeartbeatThread() run_date = dates.now() self.is_running = True self.lock_file = heartbeat.lock_file.name self.save() heartbeat.start() try: logger.debug("Calling command '%s'" % self.command) call_command(self.command, *args, **options) logger.debug("Command '%s' completed" % self.command) self.last_run_successful = True except Exception as e: # The command failed to run; log the exception t = loader.get_template('chronograph/error_message.txt') c = Context({ 'exception': unicode(e), 'traceback': ['\n'.join(traceback.format_exception(*sys.exc_info()))] }) stderr_str += t.render(c) self.last_run_successful = False # Stop the heartbeat logger.debug("Stopping heartbeat") heartbeat.stop() heartbeat.join() duration = (dates.now() - run_date).total_seconds() self.is_running = False self.lock_file = "" # Only care about minute-level resolution self.last_run = dates.make_aware( datetime(run_date.year, run_date.month, run_date.day, run_date.hour, run_date.minute)) # If this was a forced run, then don't update the # next_run date if self.force_run: logger.debug("Resetting 'force_run'") self.force_run = False else: logger.debug("Determining 'next_run'") while self.next_run < dates.now(): self.next_run = dates.make_aware( self.rrule.after(self.next_run)) logger.debug("'next_run = ' %s" % self.next_run) self.save() # If we got any output, save it to the log stdout_str += stdout.getvalue() stderr_str += stderr.getvalue() if stderr_str: # If anything was printed to stderr, consider the run # unsuccessful self.last_run_successful = False if stdout_str or stderr_str: log = Log.objects.create(job=self, run_date=run_date, stdout=stdout_str, stderr=stderr_str, success=self.last_run_successful, duration=duration) # Redirect output back to default sys.stdout = ostdout sys.stderr = ostderr
class JobRunner(object): """ Class that handles the actual running of a job. """ def __init__(self, job): self.job_id = job.id def run(self): """ This method implements the code to actually run a ``Job``. """ args = None options = None job = None last_run_successful = None heartbeat = None job_is_running = False run_date = dates.now() # Update job with running data. with transaction.commit_on_success(): job = Job.objects.lock_job(self.job_id) if not job.check_is_running(): args, options = job.get_args() heartbeat = JobHeartbeatThread() job.is_running = True job.lock_file = heartbeat.lock_file.name job.save() else: job_is_running = True # Only proceed if the job is not already running. if not job_is_running: # Redirect output so that we can log it if there is any stdout = StringIO() stderr = StringIO() ostdout = sys.stdout ostderr = sys.stderr sys.stdout = stdout sys.stderr = stderr stdout_str, stderr_str = "", "" # Start job heartbeat. heartbeat.start() try: logger.debug("Calling command '%s'" % job.command) call_command(job.command, *args, **options) logger.debug("Command '%s' completed" % job.command) last_run_successful = True except Exception, e: try: # The command failed to run; log the exception t = loader.get_template('chronograph/error_message.txt') c = Context({ 'exception': unicode(e), 'traceback': ['\n'.join(traceback.format_exception(*sys.exc_info()))] }) stderr_str += t.render(c) except Exception, e2: sys.stderr.write('Caught exception (%s) while handling job exception (%s)' % (e2, e)) last_run_successful = False # Stop the heartbeat logger.debug("Stopping heartbeat") heartbeat.stop() heartbeat.join() # Get stdout/stderr. stdout_str += stdout.getvalue() stderr_str += stderr.getvalue() duration = dates.total_seconds((dates.now()-run_date)) with transaction.commit_on_success(): job = Job.objects.lock_job(self.job_id) # If anything was printed to stderr, consider the run # unsuccessful if stderr_str: job.last_run_successful = False else: job.last_run_successful = last_run_successful job.is_running = False job.lock_file = "" # Only care about minute-level resolution job.last_run = dates.make_aware(datetime( run_date.year, run_date.month, run_date.day, run_date.hour, run_date.minute)) # If this was a forced run, then don't update the # next_run date if job.force_run: logger.debug("Resetting 'force_run'") job.force_run = False else: logger.debug("Determining 'next_run'") while job.next_run < dates.now(): job.next_run = dates.make_aware(job.rrule.after(job.next_run)) logger.debug("'next_run = ' %s" % job.next_run) job.save() # Redirect output back to default sys.stdout = ostdout sys.stderr = ostderr if stdout_str or stderr_str: log = Log.objects.create( job=job, run_date=run_date, stdout=stdout_str, stderr=stderr_str, success=job.last_run_successful, duration=duration ) # Send emails if (job.only_email_on_error and stderr_str) or (not job.only_email_on_error): print 'emailing' log.email_subscribers()