/**
  * @param string $queue
  * @return null|QueuedJobDescriptor
  */
 protected function getNextJobDescriptorWithoutMutex($queue)
 {
     $list = QueuedJobDescriptor::get()->filter('JobType', $queue)->sort('ID', 'ASC');
     $descriptor = $list->filter('JobStatus', QueuedJob::STATUS_WAIT)->first();
     if ($descriptor) {
         return $descriptor;
     }
     return $list->filter('JobStatus', QueuedJob::STATUS_NEW)->where(sprintf('"StartAfter" < \'%s\' OR "StartAfter" IS NULL', SS_DateTime::now()->getValue()))->first();
 }
 /**
  * @inheritdoc
  *
  * @throws InvalidArgumentException
  * @param string
  */
 public function unserialize($serialized)
 {
     $data = unserialize($serialized);
     if (!isset($data['descriptor'])) {
         throw new InvalidArgumentException('Malformed data');
     }
     $descriptor = QueuedJobDescriptor::get()->filter('ID', $data['descriptor'])->first();
     if (!$descriptor) {
         throw new InvalidArgumentException('Descriptor not found');
     }
     $this->descriptor = $descriptor;
 }
 /**
  *
  * Create a new {@link GenerateGoogleSitemapJob} after each CMS write manipulation.
  *
  * @param {@inheritdoc}
  * @return mixed void | null
  */
 public function onAfterPublish(&$original)
 {
     if (!class_exists('AbstractQueuedJob')) {
         return;
     }
     // Get all "running" GenerateGoogleSitemapJob's
     $list = QueuedJobDescriptor::get()->filter(array('Implementation' => 'GenerateGoogleSitemapJob', 'JobStatus' => array(QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN)));
     $existingJob = $list ? $list->first() : null;
     if ($existingJob && $existingJob->exists()) {
         // Do nothing. There's a job for generating the sitemap already running
     } else {
         $where = '"StartAfter" > \'' . date('Y-m-d H:i:s') . '\'';
         $list = QueuedJobDescriptor::get()->where($where);
         $list = $list->filter(array('Implementation' => 'GenerateGoogleSitemapJob', 'JobStatus' => array(QueuedJob::STATUS_NEW)));
         $list = $list->sort('ID', 'ASC');
         if ($list && $list->count()) {
             // Execute immediately
             $existingJob = $list->first();
             $existingJob->StartAfter = date('Y-m-d H:i:s');
             $existingJob->write();
             return;
         }
         // If no such a job existing, create a new one for the first time, and run immediately
         // But first remove all legacy jobs which might be of the following statuses:
         /**
          * New (but Start data somehow is less than now)
          * Waiting
          * Completed
          * Paused
          * Cancelled
          * Broken
          */
         $list = QueuedJobDescriptor::get()->filter(array('Implementation' => 'GenerateGoogleSitemapJob'));
         if ($list && $list->count()) {
             $list->removeAll();
         }
         $job = new GenerateGoogleSitemapJob();
         singleton('QueuedJobService')->queueJob($job);
     }
 }
 /**
  * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
  * between each run. If it's not, then we need to flag it as paused due to an error.
  *
  * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
  * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
  * fix them
  *
  * @param int $queue The queue to check against
  */
 public function checkJobHealth($queue = null)
 {
     $queue = $queue ?: QueuedJob::QUEUED;
     // Select all jobs currently marked as running
     $runningJobs = QueuedJobDescriptor::get()->filter(array('JobStatus' => array(QueuedJob::STATUS_RUN, QueuedJob::STATUS_INIT), 'JobType' => $queue));
     // If no steps have been processed since the last run, consider it a broken job
     // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
     $stalledJobs = $runningJobs->filter('LastProcessedCount:GreaterThanOrEqual', 0)->where('"StepsProcessed" = "LastProcessedCount"');
     foreach ($stalledJobs as $stalledJob) {
         $this->restartStalledJob($stalledJob);
     }
     // now, find those that need to be marked before the next check
     // foreach job, mark it as having been incremented
     foreach ($runningJobs as $job) {
         $job->LastProcessedCount = $job->StepsProcessed;
         $job->write();
     }
     // finally, find the list of broken jobs and send an email if there's some found
     $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
     if ($brokenJobs && $brokenJobs->count()) {
         SS_Log::log(array('errno' => 0, 'errstr' => 'Broken jobs were found in the job queue', 'errfile' => __FILE__, 'errline' => __LINE__, 'errcontext' => array()), SS_Log::ERR);
     }
 }
 /**
  * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
  * between each run. If it's not, then we need to flag it as paused due to an error.
  *
  * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
  * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
  * fix them
  */
 public function checkJobHealth()
 {
     // first off, we want to find jobs that haven't changed since they were last checked (assuming they've actually
     // processed a few steps...)
     $filter = singleton('QJUtils')->dbQuote(array('JobStatus =' => QueuedJob::STATUS_RUN, 'StepsProcessed >' => 0));
     $filter = $filter . ' AND "StepsProcessed"="LastProcessedCount"';
     $stalledJobs = QueuedJobDescriptor::get()->filter(array('JobStatus' => QueuedJob::STATUS_RUN, 'StepsProcessed:GreaterThan' => 0));
     $stalledJobs = $stalledJobs->where('"StepsProcessed"="LastProcessedCount"');
     if ($stalledJobs) {
         foreach ($stalledJobs as $stalledJob) {
             if ($stalledJob->ResumeCount <= self::$stall_threshold) {
                 $stalledJob->ResumeCount++;
                 $stalledJob->pause();
                 $stalledJob->resume();
                 $msg = sprintf(_t('QueuedJobs.STALLED_JOB_MSG', 'A job named %s appears to have stalled. It will be stopped and restarted, please login to make sure it has continued'), $stalledJob->JobTitle);
             } else {
                 $stalledJob->pause();
                 $msg = sprintf(_t('QueuedJobs.STALLED_JOB_MSG', 'A job named %s appears to have stalled. It has been paused, please login to check it'), $stalledJob->JobTitle);
             }
             singleton('QJUtils')->log($msg);
             $mail = new Email(Email::getAdminEmail(), Email::getAdminEmail(), _t('QueuedJobs.STALLED_JOB', 'Stalled job'), $msg);
             $mail->send();
         }
     }
     // now, find those that need to be marked before the next check
     $runningJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_RUN);
     if ($runningJobs) {
         // foreach job, mark it as having been incremented
         foreach ($runningJobs as $job) {
             $job->LastProcessedCount = $job->StepsProcessed;
             $job->write();
         }
     }
     // finally, find the list of broken jobs and send an email if there's some found
     $min = date('i');
     if ($min == '42' || true) {
         $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
         if ($brokenJobs && $brokenJobs->count()) {
             SS_Log::log(array('errno' => 0, 'errstr' => 'Broken jobs were found in the job queue', 'errfile' => __FILE__, 'errline' => __LINE__, 'errcontext' => ''), SS_Log::ERR);
         }
     }
 }
 /**
  * Verify that broken jobs are correctly verified for health and restarted as necessary
  *
  * Order of checkJobHealth() and getNextPendingJob() is important
  *
  * Execution of this job is broken into several "loops", each of which represents one invocation
  * of ProcessJobQueueTask
  */
 public function testJobHealthCheck()
 {
     // Create a job and add it to the queue
     $svc = $this->getService();
     $job = new TestQueuedJob(QueuedJob::IMMEDIATE);
     $job->firstJob = true;
     $id = $svc->queueJob($job);
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     // Verify initial state is new and LastProcessedCount is not marked yet
     $this->assertEquals(QueuedJob::STATUS_NEW, $descriptor->JobStatus);
     $this->assertEquals(0, $descriptor->StepsProcessed);
     $this->assertEquals(-1, $descriptor->LastProcessedCount);
     $this->assertEquals(0, $descriptor->ResumeCounts);
     // Loop 1 - Pick up new job and attempt to run it
     // Job health should not attempt to cleanup unstarted jobs
     $svc->checkJobHealth();
     $nextJob = $svc->getNextPendingJob(QueuedJob::IMMEDIATE);
     // Ensure that this is the next job ready to go
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     $this->assertEquals($nextJob->ID, $descriptor->ID);
     $this->assertEquals(QueuedJob::STATUS_NEW, $descriptor->JobStatus);
     $this->assertEquals(0, $descriptor->StepsProcessed);
     $this->assertEquals(-1, $descriptor->LastProcessedCount);
     $this->assertEquals(0, $descriptor->ResumeCounts);
     // Run 1 - Start the job (no work is done)
     $descriptor->JobStatus = QueuedJob::STATUS_INIT;
     $descriptor->write();
     // Assume that something bad happens at this point, the process dies during execution, and
     // the task is re-initiated somewhere down the track
     // Loop 2 - Detect broken job, and mark it for future checking.
     $svc->checkJobHealth();
     $nextJob = $svc->getNextPendingJob(QueuedJob::IMMEDIATE);
     // Note that we don't immediately try to restart it until StepsProcessed = LastProcessedCount
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     $this->assertFalse($nextJob);
     // Don't run it this round please!
     $this->assertEquals(QueuedJob::STATUS_INIT, $descriptor->JobStatus);
     $this->assertEquals(0, $descriptor->StepsProcessed);
     $this->assertEquals(0, $descriptor->LastProcessedCount);
     $this->assertEquals(0, $descriptor->ResumeCounts);
     // Loop 3 - We've previously marked this job as broken, so restart it this round
     // If no more work has been done on the job at this point, assume that we are able to
     // restart it
     $svc->checkJobHealth();
     $nextJob = $svc->getNextPendingJob(QueuedJob::IMMEDIATE);
     // This job is resumed and exeuction is attempted this round
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     $this->assertEquals($nextJob->ID, $descriptor->ID);
     $this->assertEquals(QueuedJob::STATUS_WAIT, $descriptor->JobStatus);
     $this->assertEquals(0, $descriptor->StepsProcessed);
     $this->assertEquals(0, $descriptor->LastProcessedCount);
     $this->assertEquals(1, $descriptor->ResumeCounts);
     // Run 2 - First restart (work is done)
     $descriptor->JobStatus = QueuedJob::STATUS_RUN;
     $descriptor->StepsProcessed++;
     // Essentially delays the next restart by 1 loop
     $descriptor->write();
     // Once again, at this point, assume the job fails and crashes
     // Loop 4 - Assuming a job has LastProcessedCount < StepsProcessed we are in the same
     // situation as step 2.
     // Because the last time the loop ran, StepsProcessed was incremented,
     // this indicates that it's likely that another task could be working on this job, so
     // don't run this.
     $svc->checkJobHealth();
     $nextJob = $svc->getNextPendingJob(QueuedJob::IMMEDIATE);
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     $this->assertFalse($nextJob);
     // Don't run jobs we aren't sure should be restarted
     $this->assertEquals(QueuedJob::STATUS_RUN, $descriptor->JobStatus);
     $this->assertEquals(1, $descriptor->StepsProcessed);
     $this->assertEquals(1, $descriptor->LastProcessedCount);
     $this->assertEquals(1, $descriptor->ResumeCounts);
     // Loop 5 - Job is again found to not have been restarted since last iteration, so perform second
     // restart. The job should be attempted to run this loop
     $svc->checkJobHealth();
     $nextJob = $svc->getNextPendingJob(QueuedJob::IMMEDIATE);
     // This job is resumed and exeuction is attempted this round
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     $this->assertEquals($nextJob->ID, $descriptor->ID);
     $this->assertEquals(QueuedJob::STATUS_WAIT, $descriptor->JobStatus);
     $this->assertEquals(1, $descriptor->StepsProcessed);
     $this->assertEquals(1, $descriptor->LastProcessedCount);
     $this->assertEquals(2, $descriptor->ResumeCounts);
     // Run 3 - Second and last restart (no work is done)
     $descriptor->JobStatus = QueuedJob::STATUS_RUN;
     $descriptor->write();
     // Loop 6 - As no progress has been made since loop 3, we can mark this as dead
     $svc->checkJobHealth();
     $nextJob = $svc->getNextPendingJob(QueuedJob::IMMEDIATE);
     // Since no StepsProcessed has been done, don't wait another loop to mark this as dead
     $descriptor = QueuedJobDescriptor::get()->byID($id);
     $this->assertEquals(QueuedJob::STATUS_PAUSED, $descriptor->JobStatus);
     $this->assertEmpty($nextJob);
 }
 /**
  * If the queued jobs module is installed, queue up the first job for 9am tomorrow morning
  * (by default).
  */
 public function requireDefaultRecords()
 {
     if (class_exists("ContentReviewNotificationJob")) {
         // Ensure there is not already a job queued
         if (QueuedJobDescriptor::get()->filter("Implementation", "ContentReviewNotificationJob")->first()) {
             return;
         }
         $nextRun = new ContentReviewNotificationJob();
         $runHour = Config::inst()->get("ContentReviewNotificationJob", "first_run_hour");
         $firstRunTime = date("Y-m-d H:i:s", mktime($runHour, 0, 0, date("m"), date("d") + 1, date("y")));
         singleton("QueuedJobService")->queueJob($nextRun, $firstRunTime);
         DB::alteration_message(sprintf("Added ContentReviewNotificationJob to run at %s", $firstRunTime));
     }
 }