Sfoglia il codice sorgente

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

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2816 a333f486-631f-4898-b8df-5754b55c2be0
romainq 12 anni fa
parent
commit
5394d05e53

+ 36 - 3
core/backgroundprocess.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2013 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -18,6 +18,19 @@
 
 
 /**
+ * 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();
 }
 
 ?>

+ 14 - 1
setup/applicationinstaller.class.inc.php

@@ -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);
+	}
+}

+ 28 - 8
setup/backup.class.inc.php

@@ -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.");
 		}
 	}

+ 85 - 37
webservices/cron.php

@@ -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);
+		if ($oTask->Get('total_exec_count') == 0)
+		{
+			// 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'));
-		$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'))
+
+		$oRefClass = new ReflectionClass(get_class($oProcess));
+		if ($oRefClass->implementsInterface('iScheduledProcess'))
+		{
+			// Schedules process do repeat at specific moments
+			$oPlannedStart = $oProcess->GetNextOccurrence();
+		}
+		else
 		{
-			// Huh, next planned start is already in the past, shift it of the periodicity !
-			$oPlannedStart = $oEnd->modify('+'.$oBackgroundProcess->GetPeriodicity().' seconds');
+			// 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...
-				$oTask->DBInsert();
-				if ($bVerbose)
+				$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
 				{
-					$oP->p(">> === ".$oNow->format('Y-m-d H:i:s').sprintf(" Starting:%-'=40s", ' '.$sTaskClass.' (first run) '));
+					// Background processes do start asap, i.e. "now"
+					$oTask->Set('next_run_date', $oNow->format('Y-m-d H:i:s'));
 				}
-				$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.' '));
+					$oP->p('Creating record for: '.$sTaskClass);
+					$oP->p('First execution planned at: '.$oTask->Get('next_run_date'));
 				}
+				$oTask->DBInsert();
+				$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)
 	{