diff --git a/core/asynctask.class.inc.php b/core/asynctask.class.inc.php index beaf4bc7e..5c7d329bf 100644 --- a/core/asynctask.class.inc.php +++ b/core/asynctask.class.inc.php @@ -157,7 +157,7 @@ abstract class AsyncTask extends DBObject if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) { $aConfig = $aRetries[get_class($this)]; - $iRetryDelay = $aConfig['retry_delay']; + $iRetryDelay = $aConfig['retry_delay'] ?? $iRetryDelay; } return $iRetryDelay; } @@ -169,11 +169,71 @@ abstract class AsyncTask extends DBObject if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) { $aConfig = $aRetries[get_class($this)]; - $iMaxRetries = $aConfig['max_retries']; + $iMaxRetries = $aConfig['max_retries'] ?? $iMaxRetries; } return $iMaxRetries; } + public function IsRetryDelayExponential() + { + $bExponential = false; + $aRetries = MetaModel::GetConfig()->Get('async_task_retries'); + if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) + { + $aConfig = $aRetries[get_class($this)]; + $bExponential = (bool)$aConfig['exponential_delay'] ?? $bExponential; + } + return $bExponential; + } + + public static function CheckRetryConfig(Config $oConfig, $sAsyncTaskClass) + { + $aMessages = []; + $aRetries = $oConfig->Get('async_task_retries'); + if (is_array($aRetries) && array_key_exists($sAsyncTaskClass, $aRetries)) + { + $aValidKeys = array("retry_delay", "max_retries", "exponential_delay"); + $aConfig = $aRetries[$sAsyncTaskClass]; + if (!is_array($aConfig)) + { + $aMessages[] = Dict::Format('Class:AsyncTask:InvalidConfig_Class_Keys', $sAsyncTaskClass, implode(', ', $aValidKeys)); + } + else + { + foreach($aConfig as $key => $value) + { + if (!in_array($key, $aValidKeys)) + { + $aMessages[] = Dict::Format('Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys', $sAsyncTaskClass, $key, implode(', ', $aValidKeys)); + } + } + } + } + return $aMessages; + } + + /** + * Compute the delay to wait for the "next retry", based on the given parameters + * @param bool $bIsExponential + * @param int $iRetryDelay + * @param int $iMaxRetries + * @param int $iRemainingRetries + * @return int + */ + public static function GetNextRetryDelay($bIsExponential, $iRetryDelay, $iMaxRetries, $iRemainingRetries) + { + if ($bIsExponential) + { + $iExponent = $iMaxRetries - $iRemainingRetries; + if ($iExponent < 0) $iExponent = 0; // Safety net in case on configuration change in the middle of retries + return $iRetryDelay * (2 ** $iExponent); + } + else + { + return $iRetryDelay; + } + } + /** * Override to notify people that a task cannot be performed */ @@ -241,25 +301,26 @@ abstract class AsyncTask extends DBObject if ($iRemaining > 0) { $iRetryDelay = $this->GetRetryDelay($iErrorCode); - IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage.' - remaining retries: '.$iRemaining.' - next retry in '.$iRetryDelay.'s'); + $iNextRetryDelay = static::GetNextRetryDelay($this->IsRetryDelayExponential(), $iRetryDelay, $this->GetMaxRetries($iErrorCode), $iRemaining); + IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage.' - remaining retries: '.$iRemaining.' - next retry in '.$iNextRetryDelay.'s'); if ($this->Get('event_id') != 0) { $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task. Remaining retries: '.$iRemaining.'. Next retry in '.$iRetryDelay.'s'"); + $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task. Remaining retries: $iRemaining. Next retry in {$iNextRetryDelay}s"); try { $oEventLog->DBUpdate(); } catch (Exception $e) { - $oEventLog->Set('message', "Failed to process async task. Remaining retries: '.$iRemaining.'. Next retry in '.$iRetryDelay.'s', more details in the log"); + $oEventLog->Set('message', "Failed to process async task. Remaining retries: $iRemaining. Next retry in {$iNextRetryDelay}s, more details in the log"); $oEventLog->DBUpdate(); } } $this->Set('remaining_retries', $iRemaining - 1); $this->Set('status', 'planned'); $this->Set('started', null); - $this->Set('planned', time() + $iRetryDelay); + $this->Set('planned', time() + $iNextRetryDelay); } else { diff --git a/datamodels/2.x/itop-config/config.php b/datamodels/2.x/itop-config/config.php index ce2f27a43..23a10ec6b 100644 --- a/datamodels/2.x/itop-config/config.php +++ b/datamodels/2.x/itop-config/config.php @@ -51,6 +51,30 @@ function DBPasswordInNewConfigIsOk($sSafeContent) return true; } +function CheckAsyncTasksRetryConfig(Config $oTempConfig, iTopWebPage $oP) +{ + $iWarnings = 0; + foreach(get_declared_classes() as $sPHPClass) + { + $oRefClass = new ReflectionClass($sPHPClass); + if ($oRefClass->isSubclassOf('AsyncTask') && !$oRefClass->isAbstract()) + { + $aMessages = AsyncTask::CheckRetryConfig($oTempConfig, $oRefClass->getName()); + + if (count($aMessages) !== 0) + { + foreach($aMessages as $sMessage) + { + $oAlert = AlertUIBlockFactory::MakeForWarning('', $sMessage); + $oP->AddUiBlock($oAlert); + $iWarnings ++; + } + } + } + } + return $iWarnings; +} + ///////////////////////////////////////////////////////////////////// // Main program // @@ -142,6 +166,10 @@ try { $iEditorTopMargin += 5; } $oP->AddUiBlock($oAlert); + + $iWarnings = CheckAsyncTasksRetryConfig($oTempConfig, $oP); + $iEditorTopMargin += 5*$iWarnings; + $sOriginalConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile)); } catch (Exception $e) { $oAlert = AlertUIBlockFactory::MakeForDanger('', $e->getMessage()); diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index bdf2b0242..9c37db586 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -1086,6 +1086,8 @@ Dict::Add('EN US', 'English', 'English', array( 'Class:AsyncTask/Attribute:last_error+' => '', 'Class:AsyncTask/Attribute:last_attempt' => 'Last attempt', 'Class:AsyncTask/Attribute:last_attempt+' => '', + 'Class:AsyncTask:InvalidConfig_Class_Keys' => 'Invalid format for the configuration of "async_taks_retries[%1$s]". Expecting an array with the following keys: %2$s', + 'Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys' => 'Invalid format for the configuration of "async_taks_retries[%1$s]": unexpected key "%2$s". Expecting only the following keys: %3$s', )); // diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 377ec808e..3cc1c3db9 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -1084,6 +1084,8 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Class:AsyncTask/Attribute:last_error+' => '', 'Class:AsyncTask/Attribute:last_attempt' => 'Dernière tentative', 'Class:AsyncTask/Attribute:last_attempt+' => '', + 'Class:AsyncTask:InvalidConfig_Class_Keys' => 'Format incorrect pour la configuration de "async_taks_retries[%1$s]". La bonne syntaxe est un tableau avec comme clés: %2$s', + 'Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys' => 'Format incorrect pour la configuration de "async_taks_retries[%1$s]": clé "%2$s" invalide. Les clés attendues sont: %3$s', )); //