From 00064913040804c4d33fc7f79551073c80e4492d Mon Sep 17 00:00:00 2001 From: Potherca-Bot Date: Thu, 4 Sep 2025 09:10:57 +0000 Subject: [PATCH 1/2] Adds separate file for 'ItopCounter.php'. --- core/counter.class.inc.php => sources/Core/ItopCounter.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/counter.class.inc.php => sources/Core/ItopCounter.php (100%) diff --git a/core/counter.class.inc.php b/sources/Core/ItopCounter.php similarity index 100% rename from core/counter.class.inc.php rename to sources/Core/ItopCounter.php From b803db38b91136a3faea364013c1a571b77d1fe4 Mon Sep 17 00:00:00 2001 From: Potherca-Bot Date: Thu, 4 Sep 2025 09:10:57 +0000 Subject: [PATCH 2/2] Changes content in separated file 'ItopCounter.php'. --- sources/Core/ItopCounter.php | 215 +++++++++++++++++++++++++---------- 1 file changed, 153 insertions(+), 62 deletions(-) diff --git a/sources/Core/ItopCounter.php b/sources/Core/ItopCounter.php index 9a1e0b4ee..6f7959014 100644 --- a/sources/Core/ItopCounter.php +++ b/sources/Core/ItopCounter.php @@ -1,72 +1,163 @@ '', - 'key_type' => 'autoincrement', - 'name_attcode' => array('key_name'), - 'state_attcode' => '', - 'reconc_keys' => array(''), - 'db_table' => 'key_value_store', - 'db_key_field' => 'id', - 'db_finalclass_field' => '', - 'indexes' => array ( - array ( - 0 => 'key_name', - 1 => 'namespace', - ), - ),); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("namespace", array("allowed_values"=>null, "sql"=>'namespace', "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false))); - MetaModel::Init_AddAttribute(new AttributeString("key_name", array("allowed_values"=>null, "sql"=>'key_name', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false))); - MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>'value', "default_value"=>'0', "is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false))); - MetaModel::Init_SetZListItems('details', array ( - 0 => 'key_name', - 1 => 'value', - 2 => 'namespace', - )); - MetaModel::Init_SetZListItems('standard_search', array ( - 0 => 'key_name', - 1 => 'value', - 2 => 'namespace', - )); - MetaModel::Init_SetZListItems('list', array ( - 0 => 'key_name', - 1 => 'value', - 2 => 'namespace', - )); - ; - } + /** + * Key based counter. + * The counter is protected against concurrency script. + * + * @param $sCounterName + * @param null|callable $oNewObjectValueProvider optional callable that must return an integer. Used when no key is found + * + * @return int the counter starting at + * * `0` when no $oNewObjectValueProvider is given (or null) + * * `$oNewObjectValueProvider() + 1` otherwise + * + * @throws \CoreException + * @throws \MySQLException + * @throws \Exception + */ + public static function Inc($sCounterName, $oNewObjectValueProvider = null) + { + $sSelfClassName = self::class; + $sMutexKeyName = "{$sSelfClassName}-{$sCounterName}"; + $oiTopMutex = new iTopMutex($sMutexKeyName); + $oiTopMutex->Lock(); + $bIsInsideTransaction = CMDBSource::IsInsideTransaction(); + if ($bIsInsideTransaction) { + // # Transaction isolation hack: + // When inside a transaction, we need to open a new connection for the counter. + // So it is visible immediately to the connections outside of the transaction. + // Either way, the lock is not long enought, and there would be duplicate ref. + // + // SELECT ... FOR UPDATE would have also worked but with the cost of extra long lock (until the commit), + // we did not wanted this! As opening a short connection is less prone to starving than a long running one. + // Plus it would trigger way more deadlocks! + $hDBLink = self::InitMySQLSession(); + } else { + $hDBLink = CMDBSource::GetMysqli(); + } + try { + $oFilter = DBObjectSearch::FromOQL('SELECT KeyValueStore WHERE key_name=:key_name AND namespace=:namespace', array( + 'key_name' => $sCounterName, + 'namespace' => $sSelfClassName, + )); + $oAttDef = MetaModel::GetAttributeDef(KeyValueStore::class, 'value'); + $aAttToLoad = array(KeyValueStore::class => array('value' => $oAttDef)); + $sSql = $oFilter->MakeSelectQuery(array(), array(), $aAttToLoad); + $hResult = mysqli_query($hDBLink, $sSql); + $aCounter = mysqli_fetch_array($hResult, MYSQLI_NUM); + mysqli_free_result($hResult); + + //Rebuild the filter, as the MakeSelectQuery polluted the orignal and it cannot be reused + $oFilter = DBObjectSearch::FromOQL('SELECT KeyValueStore WHERE key_name=:key_name AND namespace=:namespace', array( + 'key_name' => $sCounterName, + 'namespace' => $sSelfClassName, + )); + + if (is_null($aCounter)) { + if (null != $oNewObjectValueProvider) { + $iComputedValue = $oNewObjectValueProvider(); + } else { + $iComputedValue = 0; + } + + $iCurrentValue = $iComputedValue + 1; + + $aQueryParams = array( + 'key_name' => $sCounterName, + 'value' => "$iCurrentValue", + 'namespace' => $sSelfClassName, + ); + + $sSql = $oFilter->MakeInsertQuery($aQueryParams); + } else { + $iCurrentValue = (int)$aCounter[1]; + $iCurrentValue++; + $aQueryParams = array( + 'value' => "$iCurrentValue", + ); + + $sSql = $oFilter->MakeUpdateQuery($aQueryParams); + } + + $hResult = mysqli_query($hDBLink, $sSql); + + } catch (Exception $e) { + IssueLog::Error($e->getMessage()); + throw $e; + } finally { + if ($bIsInsideTransaction) { + mysqli_close($hDBLink); + } + $oiTopMutex->Unlock(); + } + + return $iCurrentValue; + } + + /** + * handle a counter for the root class of given $sLeafClass. + * If no counter exist initialize it with the `max(id) + 1` + * + * + * + * @param $sLeafClass + * + * @return int + * @throws \ArchivedObjectException + * @throws \CoreCannotSaveObjectException + * @throws \CoreException + * @throws \CoreOqlMultipleResultsForbiddenException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + * @throws \OQLException + */ + public static function IncClass($sLeafClass) + { + $sRootClass = MetaModel::GetRootClass($sLeafClass); + + $oNewObjectCallback = function () use ($sRootClass) { + $sRootTable = MetaModel::DBGetTable($sRootClass); + $sIdField = MetaModel::DBGetKey($sRootClass); + + return CMDBSource::QueryToScalar("SELECT max(`$sIdField`) FROM `$sRootTable`"); + }; + + return self::Inc($sRootClass, $oNewObjectCallback); + } + + /** + * @return \mysqli + * @throws \ConfigException + * @throws \CoreException + * @throws \MySQLException + */ + private static function InitMySQLSession() + { + $oConfig = utils::GetConfig(); + $sDBHost = $oConfig->Get('db_host'); + $sDBUser = $oConfig->Get('db_user'); + $sDBPwd = $oConfig->Get('db_pwd'); + $sDBName = $oConfig->Get('db_name'); + $bDBTlsEnabled = $oConfig->Get('db_tls.enabled'); + $sDBTlsCA = $oConfig->Get('db_tls.ca'); + + $hDBLink = CMDBSource::GetMysqliInstance($sDBHost, $sDBUser, $sDBPwd, $sDBName, $bDBTlsEnabled, $sDBTlsCA, false); + + if (!$hDBLink) { + throw new MySQLException('Could not connect to the DB server ' . mysqli_connect_error() . ' (mysql errno: ' . mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser)); + } + + return $hDBLink; + } } \ No newline at end of file