N°4692 - Enable parallelization of multiple CRON jobs

This commit is contained in:
Eric Espie
2026-07-17 15:52:54 +02:00
parent 557e40939a
commit 973c254548
5 changed files with 127 additions and 17 deletions

View File

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

View File

@@ -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,
],

View File

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

View File

@@ -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();

View File

@@ -0,0 +1,79 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\WebPage\CLIPage;
use Combodo\iTop\Service\Cron\CronLog;
require_once(__DIR__.'/../approot.inc.php');
$sConfigFile = utils::GetConfigFilePath();
if (!file_exists($sConfigFile)) {
echo "iTop is not yet installed. Exiting...\n";
exit(EXIT_CODE_ERROR);
}
require_once(APPROOT.'/application/startup.inc.php');
function UsageAndExit()
{
echo "USAGE:\n";
echo "php cron_multi_launcher_linux.php --param_file=<file>\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";
}