/** * Start the actual execution of a job * * This method will continue executing until the job says it's completed * * @param int $jobId * The ID of the job to start executing */ public function runJob($jobId) { // first retrieve the descriptor $jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId); if (!$jobDescriptor) { throw new Exception("{$jobId} is invalid"); } // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI, // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it // as the currently logged in user and hope for the best $originalUser = Member::currentUser(); $runAsUser = null; if (Director::is_cli() || !Member::currentUser() || Member::currentUser()->isAdmin()) { $runAsUser = $jobDescriptor->RunAs(); if ($runAsUser && $runAsUser->exists()) { // the job runner outputs content way early in the piece, meaning there'll be cooking errors // if we try and do a normal login, and we only want it temporarily... Session::set("loggedInAs", $runAsUser->ID); } } // set up a custom error handler for this processing $errorHandler = new JobErrorHandler(); $job = null; try { $job = $this->initialiseJob($jobDescriptor); // get the job ready to begin. if (!$jobDescriptor->JobStarted) { $jobDescriptor->JobStarted = date('Y-m-d H:i:s'); } else { $jobDescriptor->JobRestarted = date('Y-m-d H:i:s'); } $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN; $jobDescriptor->write(); $lastStepProcessed = 0; // have we stalled at all? $stallCount = 0; $broken = false; // while not finished while (!$job->jobFinished() && !$broken) { // see that we haven't been set to 'paused' or otherwise by another process $jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId); if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) { // we've been paused by something, so we'll just exit $job->addMessage(sprintf(_t('QueuedJobs.JOB_PAUSED', "Job paused at %s"), date('Y-m-d H:i:s'))); $broken = true; } if (!$broken) { try { $job->process(); } catch (Exception $e) { // okay, we'll just catch this exception for now $job->addMessage(sprintf(_t('QueuedJobs.JOB_EXCEPT', 'Job caused exception %s in %s at line %s'), $e->getMessage(), $e->getFile(), $e->getLine()), 'ERROR'); SS_Log::log($e, SS_Log::ERR); $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN; } // now check the job state $data = $job->getJobData(); if ($data->currentStep == $lastStepProcessed) { $stallCount++; } if ($stallCount > self::$stall_threshold) { $broken = true; $job->addMessage(sprintf(_t('QueuedJobs.JOB_STALLED', "Job stalled after %s attempts - please check"), $stallCount), 'ERROR'); $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN; } // now we'll be good and check our memory usage. If it is too high, we'll set the job to // a 'Waiting' state, and let the next processing run pick up the job. if ($this->isMemoryTooHigh()) { $job->addMessage(sprintf(_t('QueuedJobs.MEMORY_RELEASE', 'Job releasing memory and waiting (%s used)'), $this->humanReadable(memory_get_usage()))); $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT; $broken = true; } } $this->copyJobToDescriptor($job, $jobDescriptor); $jobDescriptor->write(); } // a last final save $jobDescriptor->write(); } catch (Exception $e) { // okay, we'll just catch this exception for now SS_Log::log($e, SS_Log::ERR); $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN; $jobDescriptor->write(); } $errorHandler->clear(); // okay lets reset our user if we've got an original if ($runAsUser && $originalUser) { Session::clear("loggedInAs"); if ($originalUser) { Session::set("loggedInAs", $originalUser->ID); } } }
/** * Start the actual execution of a job. * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued". * * This method will continue executing until the job says it's completed * * @param int $jobId * The ID of the job to start executing * @return boolean */ public function runJob($jobId) { // first retrieve the descriptor $jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId); if (!$jobDescriptor) { throw new Exception("{$jobId} is invalid"); } // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI, // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it // as the currently logged in user and hope for the best // We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at // this point of execution in some circumstances $originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0; $originalUser = $originalUserID ? DataObject::get_by_id('Member', $originalUserID) : null; $runAsUser = null; if (Director::is_cli() || !$originalUser || Permission::checkMember($originalUser, 'ADMIN')) { $runAsUser = $jobDescriptor->RunAs(); if ($runAsUser && $runAsUser->exists()) { // the job runner outputs content way early in the piece, meaning there'll be cookie errors // if we try and do a normal login, and we only want it temporarily... if (Controller::has_curr()) { Session::set('loggedInAs', $runAsUser->ID); } else { $_SESSION['loggedInAs'] = $runAsUser->ID; } // this is an explicit coupling brought about by SS not having // a nice way of mocking a user, as it requires session // nastiness if (class_exists('SecurityContext')) { singleton('SecurityContext')->setMember($runAsUser); } } } // set up a custom error handler for this processing $errorHandler = new JobErrorHandler(); $job = null; $broken = false; // Push a config context onto the stack for the duration of this job run. Config::nest(); if ($this->grabMutex($jobDescriptor)) { try { $job = $this->initialiseJob($jobDescriptor); // get the job ready to begin. if (!$jobDescriptor->JobStarted) { $jobDescriptor->JobStarted = date('Y-m-d H:i:s'); } else { $jobDescriptor->JobRestarted = date('Y-m-d H:i:s'); } $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN; $jobDescriptor->write(); $lastStepProcessed = 0; // have we stalled at all? $stallCount = 0; if ($job->SubsiteID && class_exists('Subsite')) { Subsite::changeSubsite($job->SubsiteID); // lets set the base URL as far as Director is concerned so that our URLs are correct $subsite = DataObject::get_by_id('Subsite', $job->SubsiteID); if ($subsite && $subsite->exists()) { $domain = $subsite->domain(); $base = rtrim(Director::protocol() . $domain, '/') . '/'; Config::inst()->update('Director', 'alternate_base_url', $base); } } // while not finished while (!$job->jobFinished() && !$broken) { // see that we haven't been set to 'paused' or otherwise by another process $jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId); if (!$jobDescriptor || !$jobDescriptor->exists()) { $broken = true; SS_Log::log(array('errno' => 0, 'errstr' => 'Job descriptor ' . $jobId . ' could not be found', 'errfile' => __FILE__, 'errline' => __LINE__, 'errcontext' => array()), SS_Log::ERR); break; } if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) { // we've been paused by something, so we'll just exit $job->addMessage(sprintf(_t('QueuedJobs.JOB_PAUSED', "Job paused at %s"), date('Y-m-d H:i:s'))); $broken = true; } if (!$broken) { try { $job->process(); } catch (Exception $e) { // okay, we'll just catch this exception for now $job->addMessage(sprintf(_t('QueuedJobs.JOB_EXCEPT', 'Job caused exception %s in %s at line %s'), $e->getMessage(), $e->getFile(), $e->getLine()), 'ERROR'); SS_Log::log($e, SS_Log::ERR); $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN; } // now check the job state $data = $job->getJobData(); if ($data->currentStep == $lastStepProcessed) { $stallCount++; } if ($stallCount > Config::inst()->get(__CLASS__, 'stall_threshold')) { $broken = true; $job->addMessage(sprintf(_t('QueuedJobs.JOB_STALLED', "Job stalled after %s attempts - please check"), $stallCount), 'ERROR'); $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN; } // now we'll be good and check our memory usage. If it is too high, we'll set the job to // a 'Waiting' state, and let the next processing run pick up the job. if ($this->isMemoryTooHigh()) { $job->addMessage(sprintf(_t('QueuedJobs.MEMORY_RELEASE', 'Job releasing memory and waiting (%s used)'), $this->humanReadable($this->getMemoryUsage()))); $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT; $broken = true; } // Also check if we are running too long if ($this->hasPassedTimeLimit()) { $job->addMessage(_t('QueuedJobs.TIME_LIMIT', 'Queue has passed time limit and will restart before continuing')); $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT; $broken = true; } } if ($jobDescriptor) { $this->copyJobToDescriptor($job, $jobDescriptor); $jobDescriptor->write(); } else { SS_Log::log(array('errno' => 0, 'errstr' => 'Job descriptor has been set to null', 'errfile' => __FILE__, 'errline' => __LINE__, 'errcontext' => array()), SS_Log::WARN); $broken = true; } } // a last final save. The job is complete by now if ($jobDescriptor) { $jobDescriptor->write(); } if (!$broken) { $job->afterComplete(); $jobDescriptor->cleanupJob(); } } catch (Exception $e) { // okay, we'll just catch this exception for now SS_Log::log($e, SS_Log::ERR); $jobDescriptor->JobStatus = QueuedJob::STATUS_BROKEN; $jobDescriptor->write(); $broken = true; } } $errorHandler->clear(); Config::unnest(); // okay let's reset our user if we've got an original if ($runAsUser && $originalUser) { Session::clear("loggedInAs"); if ($originalUser) { Session::set("loggedInAs", $originalUser->ID); } } return !$broken; }