diff --git a/core/backgroundprocess.inc.php b/core/backgroundprocess.inc.php index e19e63e79..ea5a2e4f3 100644 --- a/core/backgroundprocess.inc.php +++ b/core/backgroundprocess.inc.php @@ -18,7 +18,7 @@ /** - * Class BackgroundProcess + * interface iBackgroundProcess * Any extension that must be called regularly to be executed in the background * * @copyright Copyright (C) 2010-2012 Combodo SARL diff --git a/core/backgroundtask.class.inc.php b/core/backgroundtask.class.inc.php new file mode 100644 index 000000000..bb1300816 --- /dev/null +++ b/core/backgroundtask.class.inc.php @@ -0,0 +1,76 @@ + + + +/** + * Class BackgroundTask + * A class to record information about the execution of background processes + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ +class BackgroundTask extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "class_name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_backgroundtask", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + + MetaModel::Init_AddAttribute(new AttributeString("class_name", array("allowed_values"=>null, "sql"=>"class_name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("first_run_date", array("allowed_values"=>null, "sql"=>"first_run_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("latest_run_date", array("allowed_values"=>null, "sql"=>"latest_run_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("next_run_date", array("allowed_values"=>null, "sql"=>"next_run_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeInteger("total_exec_count", array("allowed_values"=>null, "sql"=>"total_exec_count", "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDecimal("latest_run_duration", array("allowed_values"=>null, "sql"=>"latest_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDecimal("min_run_duration", array("allowed_values"=>null, "sql"=>"min_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDecimal("max_run_duration", array("allowed_values"=>null, "sql"=>"max_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDecimal("average_run_duration", array("allowed_values"=>null, "sql"=>"average_run_duration", "digits"=> 8, "decimals"=> 3, "default_value"=>"0", "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeBoolean("running", array("allowed_values"=>null, "sql"=>"running", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('active,paused'), "sql"=>"status", "default_value"=>'active', "is_null_allowed"=>false, "depends_on"=>array()))); + } + + public function ComputeDurations($fLatestDuration) + { + $iTotalRun = $this->Get('total_exec_count'); + $fAverageDuration = ($this->Get('average_run_duration') * $iTotalRun + $fLatestDuration) / (1+$iTotalRun); + $this->Set('average_run_duration', sprintf('%.3f',$fAverageDuration)); + $this->Set('total_exec_count', 1+$iTotalRun); + if ($fLatestDuration < $this->Get('min_run_duration')) + { + $this->Set('min_run_duration', sprintf('%.3f',$fLatestDuration)); + } + if ($fLatestDuration > $this->Get('max_run_duration')) + { + $this->Set('max_run_duration', sprintf('%.3f',$fLatestDuration)); + } + $this->Set('latest_run_duration', sprintf('%.3f',$fLatestDuration)); + } +} \ No newline at end of file diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 64ead9973..8ba2b804a 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -747,6 +747,7 @@ class Config 'core/action.class.inc.php', 'core/trigger.class.inc.php', 'synchro/synchrodatasource.class.inc.php', + 'core/backgroundtask.class.inc.php', ); $this->m_aDataModels = array(); $this->m_aWebServiceCategories = array( diff --git a/core/ormstopwatch.class.inc.php b/core/ormstopwatch.class.inc.php index 5370898f3..edbe1f5d5 100644 --- a/core/ormstopwatch.class.inc.php +++ b/core/ormstopwatch.class.inc.php @@ -389,6 +389,7 @@ class CheckStopWatchThresholds implements iBackgroundProcess { foreach (MetaModel::GetClasses() as $sClass) { + $aList = array(); foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { if ($oAttDef instanceof AttributeStopWatch) @@ -400,7 +401,6 @@ class CheckStopWatchThresholds implements iBackgroundProcess $sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < NOW()"; //echo $sExpression."
\n"; $oFilter = DBObjectSearch::FromOQL($sExpression); - $aList = array(); $oSet = new DBObjectSet($oFilter); while ((time() < $iTimeLimit) && ($oObj = $oSet->Fetch())) { diff --git a/webservices/cron.php b/webservices/cron.php index 21f05f307..26d52c94e 100644 --- a/webservices/cron.php +++ b/webservices/cron.php @@ -1,5 +1,5 @@ p("USAGE:\n"); - $oP->p("php -q cron.php --auth_user= --auth_pwd= [--param_file=] [--verbose=1]\n"); + $oP->p("php cron.php --auth_user= --auth_pwd= [--param_file=] [--verbose=1] [--status_only=1]\n"); } else { - $oP->p("Optional parameters: verbose, param_file\n"); + $oP->p("Optional parameters: verbose, param_file, status_only\n"); } $oP->output(); exit -2; } +function RunTask($oBackgroundProcess, BackgroundTask $oTask, $oStartDate, $iTimeLimit) +{ + $oNow = new DateTime(); + $fStart = microtime(true); + $sMessage = $oBackgroundProcess->Process($iTimeLimit); + $fDuration = microtime(true) - $fStart; + $oTask->ComputeDurations($fDuration); + $oTask->Set('latest_run_date', $oNow->format('Y-m-d H:i:s')); + $oPlannedStart = new DateTime($oTask->Get('latest_run_date')); + // Let's assume that the task was started exactly when planned so that the schedule does no shift each time + // this allows to schedule a task everyday "around" 11:30PM for example + $oPlannedStart->modify('+'.$oBackgroundProcess->GetPeriodicity().' seconds'); + $oEnd = new DateTime(); + if ($oPlannedStart->format('U') < $oEnd->format('U')) + { + // Huh, next planned start is already in the past, shift it of the periodicity ! + $oPlannedStart = $oEnd->modify('+'.$oBackgroundProcess->GetPeriodicity().' seconds'); + } + $oTask->Set('next_run_date', $oPlannedStart->format('Y-m-d H:i:s')); + $oTask->DBUpdate(); + + return $sMessage; +} // Known limitation - the background process periodicity is NOT taken into account function CronExec($oP, $aBackgroundProcesses, $bVerbose) @@ -76,18 +99,72 @@ function CronExec($oP, $aBackgroundProcesses, $bVerbose) $iCronSleep = MetaModel::GetConfig()->Get('cron_sleep'); + $oSearch = new DBObjectSearch('BackgroundTask'); while (time() < $iTimeLimit) { + $oTasks = new DBObjectSet($oSearch); + $aTasks = array(); + while($oTask = $oTasks->Fetch()) + { + $aTasks[$oTask->Get('class_name')] = $oTask; + } foreach ($aBackgroundProcesses as $oBackgroundProcess) { - if ($bVerbose) + $sTaskClass = get_class($oBackgroundProcess); + $oNow = new DateTime(); + if (!array_key_exists($sTaskClass, $aTasks)) { - $oP->p("Processing asynchronous task: ".get_class($oBackgroundProcess)); + // New entry, let's create a new BackgroundTask record and run the task immediately + $oTask = new BackgroundTask(); + $oTask->Set('class_name', get_class($oBackgroundProcess)); + $oTask->Set('first_run_date', $oNow->format('Y-m-d H:i:s')); + $oTask->Set('total_exec_count', 0); + $oTask->Set('min_run_duration', 99999.999); + $oTask->Set('max_run_duration', 0); + $oTask->Set('average_run_duration', 0); + $oTask->Set('next_run_date', $oNow->format('Y-m-d H:i:s')); // in case of crash... + $oTask->DBInsert(); + if ($bVerbose) + { + $oP->p(">> === ".$oNow->format('Y-m-d H:i:s').sprintf(" Starting:%-'=40s", ' '.$sTaskClass.' (first run) ')); + } + $sMessage = RunTask($oBackgroundProcess, $oTask, $oNow, $iTimeLimit); + if ($bVerbose) + { + if(!empty($sMessage)) + { + $oP->p("$sTaskClass: $sMessage"); + } + $oEnd = new DateTime(); + $oP->p("<< === ".$oEnd->format('Y-m-d H:i:s').sprintf(" End of: %-'=40s", ' '.$sTaskClass.' ')); + } } - $sMessage = $oBackgroundProcess->Process($iTimeLimit); - if ($bVerbose && !empty($sMessage)) + else if( ($aTasks[$sTaskClass]->Get('status') == 'active') && ($aTasks[$sTaskClass]->Get('next_run_date') <= $oNow->format('Y-m-d H:i:s'))) { - $oP->p("Returned: $sMessage"); + $oTask = $aTasks[$sTaskClass]; + // Run the task and record its next run time + if ($bVerbose) + { + $oP->p(">> === ".$oNow->format('Y-m-d H:i:s').sprintf(" Starting:%-'=40s", ' '.$sTaskClass.' ')); + } + $sMessage = RunTask($oBackgroundProcess, $aTasks[$sTaskClass], $oNow, $iTimeLimit); + if ($bVerbose) + { + if(!empty($sMessage)) + { + $oP->p("$sTaskClass: $sMessage"); + } + $oEnd = new DateTime(); + $oP->p("<< === ".$oEnd->format('Y-m-d H:i:s').sprintf(" End of: %-'=40s", ' '.$sTaskClass.' ')); + } + } + else + { + // will run later + if (($aTasks[$sTaskClass]->Get('status') == 'active') && $bVerbose) + { + $oP->p("Skipping asynchronous task: $sTaskClass until ".$aTasks[$sTaskClass]->Get('next_run_date')); + } } } if ($bVerbose) @@ -102,6 +179,25 @@ function CronExec($oP, $aBackgroundProcesses, $bVerbose) } } +function DisplayStatus($oP) +{ + $oSearch = new DBObjectSearch('BackgroundTask'); + $oTasks = new DBObjectSet($oSearch); + $oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+'); + $oP->p('| Task Class | Status | Last Run | Next Run | Nb Run | Avg. Dur. |'); + $oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+'); + while($oTask = $oTasks->Fetch()) + { + $sTaskName = $oTask->Get('class_name'); + $sStatus = $oTask->Get('status'); + $sLastRunDate = $oTask->Get('latest_run_date'); + $sNextRunDate = $oTask->Get('next_run_date'); + $iNbRun = (int)$oTask->Get('total_exec_count'); + $sAverageRunTime = $oTask->Get('average_run_duration'); + $oP->p(sprintf('| %1$-25.25s | %2$-7s | %3$s | %4$s | %5$6d | %6$7s s |', $sTaskName, $sStatus, $sLastRunDate, $sNextRunDate, $iNbRun, $sAverageRunTime)); + } + $oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+'); +} //////////////////////////////////////////////////////////////////////////////// // // Main @@ -189,10 +285,16 @@ if ($bVerbose) $sDisplayProcesses = implode(', ', $aDisplayProcesses); $oP->p("Background processes: ".$sDisplayProcesses); } +if (utils::ReadParam('status_only', false, true /* Allow CLI */)) +{ + // Display status and exit + DisplayStatus($oP); + exit(0); +} $sLockName = 'itop.cron.php'; -$oP->p("Starting: ".time()); +$oP->p("Starting: ".time().' ('.date('Y-m-d H:i:s').')'); $res = CMDBSource::QueryToScalar("SELECT GET_LOCK('$sLockName', 1)");// timeout = 1 second (see also IS_FREE_LOCK) if (is_null($res)) { @@ -209,7 +311,8 @@ elseif ($res === '1') catch(Exception $e) { // TODO - Log ? - $oP->p("ERROR:".$e->GetMessage()); + $oP->p("ERROR:".$e->getMessage()); + $oP->p($e->getTraceAsString()); } $res = CMDBSource::QueryToScalar("SELECT RELEASE_LOCK('$sLockName')"); } @@ -219,7 +322,7 @@ else // Exit silently $oP->p("Already running..."); } -$oP->p("Exiting: ".time()); +$oP->p("Exiting: ".time().' ('.date('Y-m-d H:i:s').')'); $oP->Output(); ?>