iTop唯一性检查功能测试

唯一性检查在 CMDB 里比较重要,大部分 CI 都不希望重复。iTop 2.6 之前并没有支持唯一性检查功能,需要使用 DoCheckToWrite 函数在写入前自行检查。

老方法回顾

用DoCheckToWrite函数实现写入前的校验,比如下面的代码校验某些属性,保证其唯一性。还可以在写入前进行简单的校验,例如限制登录用户只能编辑自己link的Person。

public function DoCheckToWrite()
                {
                    parent::DoCheckToWrite();
                    $finalclass = $this->Get('finalclass');
                    
                    // friendlyname of FunctionalCI has to be unique! Currently it' not possible to define this in datamodel (xml)
                    $nameSpec = MetaModel::GetNameSpec(get_class($this));
                    $sFormat = preg_replace('/%[1-9]\$s/', '%s', $nameSpec['0']);
                    $sArg = $nameSpec['1'];
                    $oArg = array();
                    
                    /*
                     * 如果组成friendlyname的所有attribute都没有发生变化,那么不进行检查
                     * 如果不监听变化就进行检查,将导致对象无法更新
                     * server不适用name作为friendlyname,如果finalclass是Server,同时检查name和friendlyname
                     */
                    $aChanges = $this->ListChanges();
                    if($finalclass == "Server" && array_key_exists('name', $aChanges))
                    {
                        $sServer = $aChanges['name'];
                        $oSearch = DBObjectSearch::FromOQL_AllData("SELECT Server WHERE name=:name");
                        $oSet = new DBObjectSet($oSearch, array(), array('name' => $sServer));
                        if ($oSet->Count() > 0)
                        {
                            $this->m_aCheckIssues[] = Dict::Format("Class:".$finalclass."/Error:".$finalclass."MustBeUnique", $sServer);
                        }           
                    }
                    $isChanges = false;
                    foreach($sArg as $value) {
                        array_push($oArg, $this->Get($value));
                        if(array_key_exists($value, $aChanges))
                            $isChanges = true;
                    }
                    $sFunctionalCI = vsprintf("$sFormat", $oArg);
                    
                    if($isChanges) {
                        $oSearch = DBObjectSearch::FromOQL_AllData("SELECT $finalclass WHERE friendlyname=:friendlyname");
                        $oSet = new DBObjectSet($oSearch, array(), array('friendlyname' => $sFunctionalCI));
                        if ($oSet->Count() > 0)
                        {
                            $this->m_aCheckIssues[] = Dict::Format("Class:".$finalclass."/Error:".$finalclass."MustBeUnique", $sFunctionalCI);
                        }                   
                    }
                }

内置唯一性检查

2.6 新增了 uniqueness_rules ,用户可以写 XML 来定义约束规则。比起 PHP 代码,方便了不少。比如标准模型中 Person 类的唯一性检查规则:

    <class id="Person" _delta="define">
      <parent>Contact</parent>
      <properties>
        ...
        <uniqueness_rules>
          <rule id="employee_number">
            <attributes>
              <attribute id="org_id"/>
              <attribute id="employee_number"/>
            </attributes>
            <filter><![CDATA[employee_number != '']]></filter>
            <disabled>false</disabled>
            <is_blocking>true</is_blocking>
          </rule>
          <rule id="name">
            <attributes>
              <attribute id="org_id"/>
              <attribute id="name"/>
              <attribute id="first_name"/>
            </attributes>
            <filter/>
            <disabled>false</disabled>
            <is_blocking>false</is_blocking>
          </rule>
        </uniqueness_rules>
      </properties>
      <fields>
	  ...

可以看到 uniqueness_rules 在 properties 下定义。这个规则限制每个组织下不能有相同员工号的 Personid 为 name 的规则,is_blocking 为 false,表明这个规则不是强制的,只会给出警告,依然能更新成功。毕竟,同名的人挺常见的。

其中 filter 的含义是,过滤某些不关心的情况,比如这个例子中,employee_number 为空的情况并不关心,允许为空并且不认为是重复项,因此用 filter 字段把这种情况过滤掉。

唯一性检查靠谱吗

测试 MGR 的时候,有几个功能会受到影响,出现重复项。因此想测试一下唯一性检查这个重要功能是否靠谱。测试方法也时通过 parallel 并发请求 iTop 接口,调用 API 的脚本如下:

#!/bin/bash

[ $# -lt 2 ] && echo "$0 password url" && exit 1
user=admin
password=$1
url=$2
json_data='{"operation":"core/create","comment":"test mgr","class":"Person","output_fields":"id,employee_number,friendlyname","fields":{"org_id":"SELECT Organization WHERE name = \"Demo\"","name":"Xing","first_name":"Ming","employee_number":"2020"}}'

curl -s "$url/webservices/rest.php?version=1.3" -d "auth_user=$user&auth_pwd=$password&json_data=$json_data" |jq .

保存为 unique.sh,使用 parallel 并发调用:

#!/bin/bash

for id in `seq 1 10`;do echo $id;done |parallel -j 3 ./unique.sh admin http://192.168.10.101 &
for id in `seq 1 10`;do echo $id;done |parallel -j 3 ./unique.sh admin http://192.168.10.102 &
for id in `seq 1 10`;do echo $id;done |parallel -j 3 ./unique.sh admin http://192.168.10.103 &

最终结果表明,在 MGR 单主,多主以及 单节点上都出现了问题。

image
创建了5个有相同工号的联系人

代码分析

代码注释里已经写明了可能的问题。。

			// No iTopMutex so there might be concurrent access !
			// But the necessary lock would have a high performance cost :(

加锁性能代价太大。。

dbobject.class.php 中:

	/**
     * @internal
     * 
	 * @throws \CoreException
	 * @throws \OQLException
     *
	 * @since 2.6.0 N°659 uniqueness constraint
	 * @api
	 */
	protected function DoCheckUniqueness()
	{
		$sCurrentClass = get_class($this);
		$aUniquenessRules = MetaModel::GetUniquenessRules($sCurrentClass);

		foreach ($aUniquenessRules as $sUniquenessRuleId => $aUniquenessRuleProperties)
		{
			if ($aUniquenessRuleProperties['disabled'] === true)
			{
				continue;
			}

			// No iTopMutex so there might be concurrent access !
			// But the necessary lock would have a high performance cost :(
			$bHasDuplicates = $this->HasObjectsInDbForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties);
			if ($bHasDuplicates)
			{
				$bIsBlockingRule = $aUniquenessRuleProperties['is_blocking'];
				if (is_null($bIsBlockingRule))
				{
					$bIsBlockingRule = true;
				}

				$sErrorMessage = $this->GetUniquenessRuleMessage($sUniquenessRuleId);

				if ($bIsBlockingRule)
				{
					$this->m_aCheckIssues[] = $sErrorMessage;
					continue;
				}
				$this->m_aCheckWarnings[] = $sErrorMessage;
				continue;
			}
		}
	}

# 然后在 DoCheckToWrite 函数中调用唯一性检查
	/**
	 * Check integrity rules (before inserting or updating the object)
	 *
     * **This method is not meant to be called directly, use DBObject::CheckToWrite()!**
	 * Errors should be inserted in $m_aCheckIssues and $m_aCheckWarnings arrays
     *
     * @overwritable-hook You can extend this method in order to provide your own logic.
     * @see CheckToWrite()
     * @see $m_aCheckIssues
     * @see $m_aCheckWarnings
     *
	 * @throws \ArchivedObjectException
	 * @throws \CoreException
	 * @throws \OQLException
	 *
	 */
	public function DoCheckToWrite()
	{
		$this->DoComputeValues();

		$this->DoCheckUniqueness();

		$aChanges = $this->ListChanges();

		foreach($aChanges as $sAttCode => $value)
		{
			$res = $this->CheckValue($sAttCode);
			if ($res !== true)
			{
				// $res contains the error description
				$this->m_aCheckIssues[] = "Unexpected value for attribute '$sAttCode': $res";
			}
		}
		if (count($this->m_aCheckIssues) > 0)
		{
			// No need to check consistency between attributes if any of them has
			// an unexpected value
			return;
		}
		$res = $this->CheckConsistency();
		if ($res !== true)
		{
			// $res contains the error description
			$this->m_aCheckIssues[] = "Consistency rules not followed: $res";
		}

		// Synchronization: are we attempting to modify an attribute for which an external source is master?
		//
		if ($this->m_bIsInDB && $this->InSyncScope() && (count($aChanges) > 0))
		{
			foreach($aChanges as $sAttCode => $value)
			{
				$iFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
				if ($iFlags & OPT_ATT_SLAVE)
				{
					// Note: $aReasonInfo['name'] could be reported (the task owning the attribute)
					$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
					$sAttLabel = $oAttDef->GetLabel();
					if (!empty($aReasons))
					{
						// Todo: associate the attribute code with the error
						$this->m_aCheckIssues[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $sAttLabel);
					}
				}
			}
		}
	}

注意到 DoCheckToWrite 的注释,This method is not meant to be called directly, use DBObject::CheckToWrite()!,看来我之前的做法也不完全正确。

结论

唯一性检查未使用 iTopMutex,即和 GET_LOCK 无关,和MGR也无关。在实践中,如果出现并发创建,是有一定几率出现重复项目的。不过也无需太过担心,实践中,两人同时创建相同对象的情况应该是很少的。如果出现了,也可以用 DB工具 来发现并修复问题。

image
System 下的 DB 工具

(全文完)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注