New capability for CRON: handle tasks scheduled at given date/time (as opposed to a task being executed more or less continuously).

SVN:trunk[2816]
This commit is contained in:
Romain Quetiez
2013-08-08 15:23:05 +00:00
parent 9536c99422
commit 98a1242050
4 changed files with 168 additions and 54 deletions

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2010-2012 Combodo SARL
// Copyright (C) 2010-2013 Combodo SARL
//
// This file is part of iTop.
//
@@ -17,6 +17,19 @@
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* interface iProcess
* Something that can be executed
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
interface iProcess
{
public function Process($iUnixTimeLimit);
}
/**
* interface iBackgroundProcess
* Any extension that must be called regularly to be executed in the background
@@ -25,10 +38,30 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
interface iBackgroundProcess
interface iBackgroundProcess extends iProcess
{
/*
Gives the repetition rate in seconds
@returns integer
*/
public function GetPeriodicity();
public function Process($iUnixTimeLimit);
}
/**
* interface iScheduledProcess
* A variant of process that must be called at specific times
*
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
interface iScheduledProcess extends iProcess
{
/*
Gives the exact time at which the process must be run next time
@returns DateTime
*/
public function GetNextOccurrence();
}
?>

View File

@@ -418,7 +418,7 @@ class ApplicationInstaller
protected static function DoBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sBackupFile, $sSourceConfigFile)
{
$oBackup = new DBBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix);
$oBackup = new SetupDBBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix);
$sZipFile = $oBackup->MakeName($sBackupFile);
$oBackup->CreateZip($sZipFile, $sSourceConfigFile);
}
@@ -867,3 +867,16 @@ class ApplicationInstaller
MetaModel::ResetCache();
}
}
class SetupDBBackup extends DBBackup
{
protected function LogInfo($sMsg)
{
SetupPage::log('Info - '.$sMsg);
}
protected function LogError($sMsg)
{
SetupPage::log('Error - '.$sMsg);
}
}

View File

@@ -23,6 +23,14 @@ class BackupException extends Exception
class DBBackup
{
// To be overriden depending on the expected usages
protected function LogInfo($sMsg)
{
}
protected function LogError($sMsg)
{
}
protected $sDBHost;
protected $iDBPort;
protected $sDBUser;
@@ -72,6 +80,16 @@ class DBBackup
$this->sDBSubName = $sDBSubName;
}
protected $sMySQLBinDir = '';
/**
* Create a normalized backup name, depending on the current date/time and Database
* @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs
*/
public function SetMySQLBinDir($sMySQLBinDir)
{
$this->sMySQLBinDir = $sMySQLBinDir;
}
/**
* Create a normalized backup name, depending on the current date/time and Database
* @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs
@@ -92,7 +110,7 @@ class DBBackup
// Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
// (delete it before spawning a process)
$sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
SetupPage::log("Info - Data file: '$sDataFile'");
$this->LogInfo("Data file: '$sDataFile'");
if (is_null($sSourceConfigFile))
{
@@ -147,7 +165,9 @@ class DBBackup
$sTables = implode(' ', $aEscapedTables);
}
$sMySQLBinDir = utils::ReadParam('mysql_bindir', '', true);
$this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')");
$sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true);
if (empty($sMySQLBinDir))
{
$sMySQLDump = 'mysqldump';
@@ -173,17 +193,17 @@ class DBBackup
$sCommandDisplay = "$sMySQLDump --opt --default-character-set=utf8 --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx --password=xxxxx --result-file=$sTmpFileName $sDBName $sTables";
// Now run the command for real
SetupPage::log("Info - Executing command: $sCommandDisplay");
$this->LogInfo("Executing command: $sCommandDisplay");
$aOutput = array();
$iRetCode = 0;
exec($sCommand, $aOutput, $iRetCode);
foreach($aOutput as $sLine)
{
SetupPage::log("Info - mysqldump said: $sLine");
$this->LogInfo("mysqldump said: $sLine");
}
if ($iRetCode != 0)
{
SetupPage::log("Error - retcode=".$iRetCode."\n");
$this->LogError("retcode=".$iRetCode."\n");
throw new BackupException("Failed to execute mysqldump. Return code: $iRetCode. Check the log file '".realpath(APPROOT.'/log/setup.log')."' for more information.");
}
}
@@ -210,17 +230,17 @@ class DBBackup
if ($oZip->close())
{
SetupPage::log("Info - Archive: $sZipArchiveFile created");
$this->LogInfo("Archive: $sZipArchiveFile created");
}
else
{
SetupPage::log("Error - Failed to save zip archive: $sZipArchiveFile");
$this->LogError("Failed to save zip archive: $sZipArchiveFile");
throw new BackupException("Failed to save zip archive: $sZipArchiveFile");
}
}
else
{
SetupPage::log("Error - Failed to create zip archive: $sZipArchiveFile.");
$this->LogError("Failed to create zip archive: $sZipArchiveFile.");
throw new BackupException("Failed to create zip archive: $sZipArchiveFile.");
}
}

View File

@@ -61,26 +61,43 @@ function UsageAndExit($oP)
exit -2;
}
function RunTask($oBackgroundProcess, BackgroundTask $oTask, $oStartDate, $iTimeLimit)
function RunTask($oProcess, BackgroundTask $oTask, $oStartDate, $iTimeLimit)
{
try
{
$oNow = new DateTime();
$fStart = microtime(true);
$sMessage = $oBackgroundProcess->Process($iTimeLimit);
$sMessage = $oProcess->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'))
if ($oTask->Get('total_exec_count') == 0)
{
// Huh, next planned start is already in the past, shift it of the periodicity !
$oPlannedStart = $oEnd->modify('+'.$oBackgroundProcess->GetPeriodicity().' seconds');
// First execution
$oTask->Set('first_run_date', $oNow->format('Y-m-d H:i:s'));
}
$oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics
$oTask->Set('latest_run_date', $oNow->format('Y-m-d H:i:s'));
$oRefClass = new ReflectionClass(get_class($oProcess));
if ($oRefClass->implementsInterface('iScheduledProcess'))
{
// Schedules process do repeat at specific moments
$oPlannedStart = $oProcess->GetNextOccurrence();
}
else
{
// Background processes do repeat periodically
$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('+'.$oProcess->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('+'.$oProcess->GetPeriodicity().' seconds');
}
}
$oTask->Set('next_run_date', $oPlannedStart->format('Y-m-d H:i:s'));
$oTask->DBUpdate();
}
@@ -91,8 +108,7 @@ function RunTask($oBackgroundProcess, BackgroundTask $oTask, $oStartDate, $iTime
return $sMessage;
}
// Known limitation - the background process periodicity is NOT taken into account
function CronExec($oP, $aBackgroundProcesses, $bVerbose)
function CronExec($oP, $aProcesses, $bVerbose)
{
$iStarted = time();
$iMaxDuration = MetaModel::GetConfig()->Get('cron_max_execution_time');
@@ -103,6 +119,26 @@ function CronExec($oP, $aBackgroundProcesses, $bVerbose)
$oP->p("Planned duration = $iMaxDuration seconds");
}
// Reset the next planned execution to take into account new settings
$oSearch = new DBObjectSearch('BackgroundTask');
$oTasks = new DBObjectSet($oSearch);
while($oTask = $oTasks->Fetch())
{
$sTaskClass = $oTask->Get('class_name');
$oRefClass = new ReflectionClass($sTaskClass);
if ($oRefClass->implementsInterface('iScheduledProcess'))
{
if ($bVerbose)
{
$oP->p("Resetting the next run date for $sTaskClass");
}
$oProcess = $aProcesses[$sTaskClass];
$oNextOcc = $oProcess->GetNextOccurrence();
$oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s'));
$oTask->DBUpdate();
}
}
$iCronSleep = MetaModel::GetConfig()->Get('cron_sleep');
$oSearch = new DBObjectSearch('BackgroundTask');
@@ -114,46 +150,47 @@ function CronExec($oP, $aBackgroundProcesses, $bVerbose)
{
$aTasks[$oTask->Get('class_name')] = $oTask;
}
foreach ($aBackgroundProcesses as $oBackgroundProcess)
foreach ($aProcesses as $oProcess)
{
$sTaskClass = get_class($oBackgroundProcess);
$sTaskClass = get_class($oProcess);
$oNow = new DateTime();
if (!array_key_exists($sTaskClass, $aTasks))
{
// New entry, let's create a new BackgroundTask record and run the task immediately
// New entry, let's create a new BackgroundTask record, and plan the first execution
$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('class_name', get_class($oProcess));
$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...
$oRefClass = new ReflectionClass($sTaskClass);
if ($oRefClass->implementsInterface('iScheduledProcess'))
{
$oNextOcc = $oProcess->GetNextOccurrence();
$oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s'));
}
else
{
// Background processes do start asap, i.e. "now"
$oTask->Set('next_run_date', $oNow->format('Y-m-d H:i:s'));
}
if ($bVerbose)
{
$oP->p('Creating record for: '.$sTaskClass);
$oP->p('First execution planned at: '.$oTask->Get('next_run_date'));
}
$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.' '));
}
$aTasks[$oTask->Get('class_name')] = $oTask;
}
else if( ($aTasks[$sTaskClass]->Get('status') == 'active') && ($aTasks[$sTaskClass]->Get('next_run_date') <= $oNow->format('Y-m-d H:i:s')))
if( ($aTasks[$sTaskClass]->Get('status') == 'active') && ($aTasks[$sTaskClass]->Get('next_run_date') <= $oNow->format('Y-m-d H:i:s')))
{
$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);
$sMessage = RunTask($oProcess, $aTasks[$sTaskClass], $oNow, $iTimeLimit);
if ($bVerbose)
{
if(!empty($sMessage))
@@ -204,10 +241,14 @@ function DisplayStatus($oP)
}
$oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+');
}
////////////////////////////////////////////////////////////////////////////////
//
// Main
//
set_time_limit(0); // Some background actions may really take long to finish (like backup)
if (utils::IsModeCLI())
{
$oP = new CLIPage("iTop - CRON");
@@ -260,21 +301,28 @@ if (!UserRights::IsAdministrator())
exit -1;
}
if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE))
{
$oP->p("A database maintenance is ongoing (read-only mode even for admins).");
$oP->Output();
exit -1;
}
// Enumerate classes implementing BackgroundProcess
//
$aBackgroundProcesses = array();
$aProcesses = array();
foreach(get_declared_classes() as $sPHPClass)
{
$oRefClass = new ReflectionClass($sPHPClass);
$oExtensionInstance = null;
if ($oRefClass->implementsInterface('iBackgroundProcess'))
if ($oRefClass->implementsInterface('iProcess'))
{
if (is_null($oExtensionInstance))
{
$oExecInstance = new $sPHPClass;
}
$aBackgroundProcesses[$sPHPClass] = $oExecInstance;
$aProcesses[$sPHPClass] = $oExecInstance;
}
}
@@ -284,7 +332,7 @@ $bVerbose = utils::ReadParam('verbose', false, true /* Allow CLI */);
if ($bVerbose)
{
$aDisplayProcesses = array();
foreach ($aBackgroundProcesses as $oExecInstance)
foreach ($aProcesses as $oExecInstance)
{
$aDisplayProcesses[] = get_class($oExecInstance);
}
@@ -318,7 +366,7 @@ elseif ($res === '1')
// The current session holds the lock
try
{
CronExec($oP, $aBackgroundProcesses, $bVerbose);
CronExec($oP, $aProcesses, $bVerbose);
}
catch(Exception $e)
{