From 973c254548e7cdb255f7e200d0ffd53eff81929b Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Fri, 17 Jul 2026 15:52:54 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B04692=20-=20Enable=20parallelization=20of?= =?UTF-8?q?=20multiple=20CRON=20jobs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/utils.inc.php | 20 ++++++ core/config.class.inc.php | 4 +- sources/Service/Cron/CronLog.php | 4 +- webservices/cron.php | 37 +++++++---- webservices/cron_multi_launcher_linux.php | 79 +++++++++++++++++++++++ 5 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 webservices/cron_multi_launcher_linux.php diff --git a/application/utils.inc.php b/application/utils.inc.php index 593f8251bc..152bfa3452 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -3218,4 +3218,24 @@ TXT return (int)$sLimit; } } + + /** + * Count the number of cron.php processes currently running + * + * @return int process count + * @throws \Exception + */ + public static function CountCronRunning(): int + { + $iMaxCronProcess = max(MetaModel::GetConfig()->Get('cron.max_processes'), 1); + $iCount = 0; + for ($i = 0; $i < $iMaxCronProcess; $i++) { + $oMutex = new iTopMutex("cron#$i"); + if ($oMutex->IsLocked()) { + $iCount++; + } + } + + return $iCount; + } } diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 24aa3a52b5..fdad731f6d 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -578,8 +578,8 @@ class Config 'cron_max_execution_time' => [ 'type' => 'integer', 'description' => 'Duration (seconds) of the cron.php script : if exceeded the script will exit even if there are remaining tasks to process. Must be shorter than php max_execution_time setting (note than when using CLI, this is set to 0 by default which means unlimited). If cron.php is ran via web, it must be shorter than the web server response timeout.', - 'default' => 600, - 'value' => 600, + 'default' => 590, + 'value' => 590, 'source_of_value' => '', 'show_in_conf_sample' => true, ], diff --git a/sources/Service/Cron/CronLog.php b/sources/Service/Cron/CronLog.php index 91295257ee..0036de2a0b 100644 --- a/sources/Service/Cron/CronLog.php +++ b/sources/Service/Cron/CronLog.php @@ -32,7 +32,9 @@ class CronLog extends LogAPI public static function Log($sLevel, $sMessage, $sChannel = null, $aContext = []): void { - $sMessage = 'cron'.str_pad(static::$iProcessNumber, 3).$sMessage; + if (static::$iProcessNumber !== 0) { + $sMessage = 'cron'.str_pad(static::$iProcessNumber, 3).$sMessage; + } parent::Log($sLevel, $sMessage, $sChannel, $aContext); } diff --git a/webservices/cron.php b/webservices/cron.php index 901d16244c..dc65f8cf31 100644 --- a/webservices/cron.php +++ b/webservices/cron.php @@ -37,9 +37,7 @@ if (file_exists(READONLY_MODE_FILE)) { exit(EXIT_CODE_ERROR); } -require_once(APPROOT.'/application/utils.inc.php'); - -$sConfigFile = APPCONF.ITOP_DEFAULT_ENV.'/'.ITOP_CONFIG_FILE; +$sConfigFile = utils::GetConfigFilePath(); if (!file_exists($sConfigFile)) { echo "iTop is not yet installed. Exiting...\n"; exit(EXIT_CODE_ERROR); @@ -53,7 +51,7 @@ function ReadMandatoryParam($oP, $sParam, $sSanitizationFilter = 'parameter') { $sValue = utils::ReadParam($sParam, null, true, $sSanitizationFilter); if (is_null($sValue)) { - $oP->p("ERROR: Missing argument '$sParam'\n"); + CronLog::Error("ERROR: Missing argument '$sParam'\n"); UsageAndExit($oP); } @@ -191,7 +189,15 @@ function CronExec($bDebug) CronLog::Trace("Planned duration per task = $iTimeSlot seconds"); CronLog::Trace("Loop pause = $iCronSleep seconds"); - ReSyncProcesses($bDebug); + $oResyncMutex = new iTopMutex("cron_resync_processes"); + try { + if ($oResyncMutex->TryLock()) { + ReSyncProcesses($bDebug); + $oResyncMutex->Unlock(); + } + } catch (Exception $e) { + CronLog::Error('Error: '.$e->GetMessage(), CronLog::CHANNEL_DEFAULT, ['stack' => $e->getTraceAsString()]); + } while (time() < $iTimeLimit) { CheckMaintenanceMode(); @@ -436,6 +442,7 @@ function ReSyncProcesses($bDebug) // Main // +CronLog::Enable(APPROOT.'/log/itop-cron.log'); set_time_limit(0); // Some background actions may really take long to finish (like backup) $bIsModeCLI = utils::IsModeCLI(); if ($bIsModeCLI) { @@ -464,7 +471,7 @@ try { if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) { UserRights::Login($sAuthUser); // Login & set the user's language } else { - $oP->p("Access wrong credentials ('$sAuthUser')"); + CronLog::Error("Access wrong credentials ('$sAuthUser')"); $oP->output(); exit(EXIT_CODE_ERROR); } @@ -474,7 +481,7 @@ try { } if (!UserRights::IsAdministrator()) { - $oP->p("Access restricted to administrators"); + CronLog::Error("Access restricted to administrators"); $oP->Output(); exit(EXIT_CODE_ERROR); } @@ -487,12 +494,11 @@ try { require_once(APPROOT.'core/mutex.class.inc.php'); } catch (Exception $e) { - $oP->p("Error: ".$e->GetMessage()); + CronLog::Error("Error: ".$e->GetMessage(), CronLog::CHANNEL_DEFAULT, ['stack' => $e->getTraceAsString()]); $oP->output(); exit(EXIT_CODE_FATAL); } -CronLog::Enable(APPROOT.'/log/error.log'); try { if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE)) { CronLog::Debug("A maintenance is ongoing"); @@ -511,11 +517,13 @@ try { } if ($bCanRun) { CronLog::$iProcessNumber = $iProcessNumber; - CronLog::Debug('Starting: '.time().' ('.date('Y-m-d H:i:s').')'); + $iCronRunningCount = utils::CountCronRunning(); + CronLog::Info("Starting ($iCronRunningCount total cron running)"); CronExec($iVerbose > 0); } else { - CronLog::$iProcessNumber = $iMaxCronProcess + 1; - CronLog::Trace("The limit of $iMaxCronProcess cron process running in parallel is already reached"); + CronLog::$iProcessNumber = 0; + CronLog::Debug("The limit of $iMaxCronProcess cron process running in parallel is already reached"); + exit(0); } } } catch (Exception $e) { @@ -524,9 +532,10 @@ try { try { $oMutex->Unlock(); } catch (Exception $e) { - $oP->p("ERROR: '".$e->getMessage()."'"); + CronLog::Error("ERROR: '".$e->getMessage()."'"); } } -CronLog::Debug("Exiting: ".time().' ('.date('Y-m-d H:i:s').')'); +$iCronRunningCount = utils::CountCronRunning(); +CronLog::Info("Exiting ($iCronRunningCount total cron running)"); $oP->Output(); diff --git a/webservices/cron_multi_launcher_linux.php b/webservices/cron_multi_launcher_linux.php new file mode 100644 index 0000000000..821e58cac4 --- /dev/null +++ b/webservices/cron_multi_launcher_linux.php @@ -0,0 +1,79 @@ +\n"; + exit(EXIT_CODE_FATAL); +} + +function ReadMandatoryParam($sParam, $sSanitizationFilter = 'parameter') +{ + $sValue = utils::ReadParam($sParam, null, true, $sSanitizationFilter); + if (is_null($sValue)) { + echo "ERROR: Missing argument '$sParam'\n"; + UsageAndExit(); + } + + return trim($sValue); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Main +// + +try { + utils::UseParamFile(); + $sAuthUser = ReadMandatoryParam('auth_user', 'raw_data'); + $sAuthPwd = ReadMandatoryParam('auth_pwd', 'raw_data'); + if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) { + UserRights::Login($sAuthUser); // Login & set the user's language + } else { + echo "Access wrong credentials ('$sAuthUser')\n"; + exit(EXIT_CODE_ERROR); + } + + if (!UserRights::IsAdministrator()) { + echo "Access restricted to administrators\n"; + exit(EXIT_CODE_ERROR); + } + + if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE) || file_exists(MAINTENANCE_MODE_FILE) || file_exists(READONLY_MODE_FILE)) { + echo "A maintenance is ongoing\n"; + exit(EXIT_CODE_ERROR); + } + + $iMaxCronProcess = max(MetaModel::GetConfig()->Get('cron.max_processes'), 1); + $iRespawnTime = 60 / $iMaxCronProcess; + for ($i = 0; $i < $iMaxCronProcess; $i++) { + $sParamsFile = utils::ReadParam('param_file', '', true, 'raw_data'); + $sCronCmd = APPROOT."webservices/cron.php --param_file=$sParamsFile"; + $sOutputFile = APPROOT.'log/itop-cron.log'; + + // Execute command, redirect stdout and stderr + exec(sprintf('/usr/bin/php %s >> %s 2>&1 &', escapeshellcmd($sCronCmd), escapeshellarg($sOutputFile))); + sleep((int)(floor($iRespawnTime))); + } + +} catch (Exception $e) { + echo "ERROR: {$e->getMessage()}\n"; +}