N°9165 - Allow direct suppression or delayed suppression depending on the amount of data to remove

This commit is contained in:
odain
2026-04-27 17:00:12 +02:00
parent 309f8f8727
commit 5d532a7879
2 changed files with 157 additions and 70 deletions

View File

@@ -3190,4 +3190,48 @@ TXT
}
}
}
public static function GetMemoryLimit(): int
{
$sLimit = ini_get('memory_limit');
if ($sLimit == '-1') {
return 128 * 1048576;
}
switch (substr($sLimit, -1)) {
case 'M':
case 'm':
return (int)$sLimit * 1048576;
case 'K':
case 'k':
return (int)$sLimit * 1024;
case 'G':
case 'g':
return (int)$sLimit * 1073741824;
default:
return (int)$sLimit;
}
}
/**
* @param int $iMaxTime
* @param int $iMaxMemory
*
* @return bool
*/
public static function ShouldStopExecution(int $iMaxTime = 0, int $iMaxMemoryPercent = 100): bool
{
if (($iMaxTime != 0) && (time() > $iMaxTime)) {
\IssueLog::Debug(__METHOD__.' timeout '.time()." (current) > $iMaxTime (max)");
return true;
}
$iMemory = memory_get_usage(true);
$iMaxMemory = self::GetMemoryLimit() * $iMaxMemoryPercent / 100;
if ($iMemory > $iMaxMemory) {
\IssueLog::Debug(__METHOD__." Memory limit $iMemory (current) > $iMaxMemory (max)");
return true;
}
return false;
}
}

View File

@@ -2,34 +2,19 @@
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBObjectSet;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use DBObject;
use DBObjectSearch;
use DeletionPlan;
use MetaModel;
use utils;
class DeletionPlanService
{
private static DeletionPlanService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DeletionPlanService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DeletionPlanService();
}
return self::$oInstance;
}
final public static function SetInstance(?DeletionPlanService $oInstance): void
{
self::$oInstance = $oInstance;
}
private array $aVisited = [];
/**
* Get a summary of the deletion plan computed for the classes.
@@ -88,73 +73,131 @@ class DeletionPlanService
return $oSet->ToArray();
}
/**
* @param array $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteDeletionPlan(array $aClasses): array
private function GetNextObjectToDelete(array $aClasses): ?DBObject
{
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
if (count($oDeletionPlan->GetIssues()) > 0) {
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
foreach ($aClasses as $sClass) {
$oFilter = new DBObjectSearch($sClass);
$oFilter->AllowAllData();
$oSet = new \DBObjectSet($oFilter);
$oObject = $oSet->Fetch();
if (! is_null($oObject)) {
return $oObject;
}
}
$aSummary = [];
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
return null;
}
foreach ($aToUpdate as $aData) {
$oToUpdate = $aData['to_reset'];
/** @var \DBObject $oToUpdate */
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
}
$oToUpdate->DBUpdate();
$oDeletionPlanSummaryEntity->iUpdateCount++;
private function Update(DBObject $oToUpdate, string $sAttCode, $value)
{
$oToUpdate->Set($sAttCode, $value);
$oToUpdate->DBUpdate();
}
private function Delete(string $sClass, string $sId)
{
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.': Cleanup failed', $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
}
/**
* @param array $aClasses
* @param int $iMaxExecutionTime
* @param int $iMaxMemoryPercent
* @return void
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
public function ExecuteDeletionPlan(array $aClasses, int $iMaxExecutionTime = 30, int $iMaxMemoryPercent = 80): void
{
$oObject = $this->GetNextObjectToDelete($aClasses);
if (is_null($oObject)) {
return;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
$iMaxTime = time() + $iMaxExecutionTime;
$this->RecursiveDeletion($oObject, $iMaxTime, $iMaxMemoryPercent);
}
foreach ($aDeletes as $sId => $aDelete) {
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
public function IsVisited(DBObject $oObject): bool
{
$sClass = get_class($oObject);
$sId = $oObject->GetKey();
$sKey = "{$sClass}_{$sId}";
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
$bRes = $this->aVisited[$sKey] ?? false;
$this->aVisited[$sKey] = true;
return $bRes;
}
private function RecursiveDeletion(DBObject $oObjectToClean, int $iMaxTime, int $iMaxMemoryPercent): void
{
if (utils::ShouldStopExecution($iMaxTime, $iMaxMemoryPercent)) {
return;
}
$sClass = get_class($oObjectToClean);
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
/** @var \AttributeExternalKey $oExtKeyAttDef */
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
// skip if this external key is behind an external field
if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) {
continue;
}
$oSearch = new DBObjectSearch($sRemoteClass);
$oSearch->AddCondition($sExtKeyAttCode, $oObjectToClean->GetKey(), '=');
$oSearch->AllowAllData();
$oSet = new CMDBObjectSet($oSearch);
$oSet->OptimizeColumnLoad([$sRemoteClass => ['id', $oExtKeyAttDef->GetCode()]]);
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
throw new DataFeatureRemovalException("DEL_MANUAL object");
}
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.': Cleanup failed', $e);
CMDBSource::Query('ROLLBACK');
throw $e;
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) {
// Move the child up one level i.e. set the same parent as the current object
$iParentId = $oObjectToClean->Get($oExtKeyAttDef->GetCode());
$this->Update($oDependentObj, $oExtKeyAttDef->GetCode(), $iParentId);
} else {
$this->Update($oDependentObj, $oExtKeyAttDef->GetCode(), 0);
}
} else {
if ($this->IsVisited($oDependentObj)) {
continue;
}
$this->RecursiveDeletion($oDependentObj, $iMaxTime, $iMaxMemoryPercent);
}
}
$oDeletionPlanSummaryEntity->iDeleteCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
$this->Delete($sClass, $oObjectToClean->GetKey());
}
/**