唯一性检查在 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
下定义。这个规则限制每个组织下不能有相同员工号的 Person
,id
为 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 单主,多主以及 单节点上都出现了问题。
代码分析
代码注释里已经写明了可能的问题。。
// 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工具 来发现并修复问题。
(全文完)
发表回复