储量对象多出来了?

问题描述

2021年10月,技术部反馈,在操作储量的时候,保存的时候正常,再次打开就会多出一些储量对象。而且这个情况还不一定出现,需要操作多次。

问题排查、重现、分析

技术部人员给录制了一个视频,视频足足有12分钟,视频里面就是在重复(1)粘贴储量对象;(2)双击对象,弹出对话框修改属性,确定;(3)移动该储量对象到合适的位置。

为了确保不是用户误操作(是不是有Ctrl+C),我把视频每一帧跟踪了一遍。同时记录每个储量对象添加的坐标以及包含的主要操作,用来与技术部发过来的图纸进行比较。光是记录这些坐标就用了1个小时。

添加日志后发现,同一个实体有的时候会被添加两次(如sysId为3的对象,第一次添加得到了dbId3,可能紧接着又再次在数据库中添加得到dbId4),确实如技术部人员所说并非每次都会出现,这就让排查工作更加复杂了。但是还好总归是可以重现的。

最终定位到实体的copy中,有些时候正常的dbId(非-1)被重置为-1了,这样导致了这个实体被重复添加了。怀疑可能有多线程导致问题,但是自动保存一直是在onTimer中。打印出来的线程Id也说明了这些动作都是在同一个线程中完成的。

前一天晚上找到23:00没有定位到问题,终于第二天早上发现了这个copy位置的错误,进而也就根据堆栈找到了问题所在。

症结所在

直接原因

用户在粘贴生成一个储量对象(假设代号为C)后,这个时候迅速双击该对象,弹出储量实体的对话框。在储量EPE中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL CEntityParser_ChuLiang::ParseCustomPropertyDlg(ILoMapCtrl* pMapCtrl, LONG layerId, LONG entityID, LONG flag, COOR* pPosition)
{
ILoVectorLayerPtr pLayer = pMapCtrl->Map->GetLayer( layerId );
ILoGeoEntityPtr pEntity = ( ( ILoGeometryEntitySetPtr )pLayer->EntitySet )->EntityByID( entityID ); // (1)
AFX_MANAGE_STATE( AfxGetStaticModuleState() );
CChuliangInfoDlg dlg( pEntity );
if ( IDOK == dlg.DoModal() )
{
if ( VARIANT_FALSE == pEntity->GetHaveAttribute( STR_MAPSCALE) )
pEntity->AddAttribute( STR_MAPSCALE, pMapCtrl->Map->MapScale );
else
pEntity->PutAttributeByName( STR_MAPSCALE, pMapCtrl->Map->MapScale );
ParseGeometry( pEntity );
( ( ILoGeometryEntitySetPtr )pLayer->EntitySet )->UpdateEntity( pEntity );// (2)
pLayer->PutEntityStyle( pEntity->SysID, GetDefaultStyle( pEntity ) );
pMapCtrl->Refresh();
}
return TRUE;
}

由于操作的速度够快,在步骤(1)执行的时候自动保存还没有完成,但是在步骤(2)之前自动保存执行了。即按照(1)->自动保存->(2)这样的步骤来完成实体的操作。由于执行(1)的时候,自动保存还没有执行,所以返回的pEntity中的dbId为-1(表示添加),sysId为3。接下来执行自动保存,自动保存正常执行,在数据库中添加了一个对象,并将dbId=3返回给了数据集缓存中的sysId=3的实体对象。然后执行(2),这一次又将pEntity的错误dbId(-1)传给了数据集缓存,进而把sysId=3的实体的dbId再次设置成了-1,最终下一次的自动保存就会把这个实体重新添加一次(得到dbId-4)。

如果操作的时候动作稍微缓和一点,添加后等待自动保存完成,然后步骤(1)就能得到正常的dbId,这样就不会出错。这也就是为什么有些情况出错,有些情况正常。当然如果动作再快一点,让自动保存在(2)以后执行,数据也会正常。

根源所在

最终问题定位到了UpdateEntity函数,也就是底层的实体更新代码中。外部调用者由于拷贝了个实体出来,他无法保证dbId的实时准确性,而只能保证sysId准确。这时候在EntityByID和UpdateEntity中间,可能这个对象的dbId已经升级了。所以我们要防止更新实体时对合法dbId的修改。

1
2
3
4
5
6
7
8
// 外部传进来的dbId可能是旧的
auto odbId = pOriGeoEntity->dbID();
pOriGeoEntity->copy(pGeoEntity);
// 恢复dbId
if (odbId != pOriGeoEntity->dbID())
{
pOriGeoEntity->dbID(odbId);
}

同理,在undo处理时,copy操作也要做相应的处理(奇怪的时undo早已经有了相应的代码)。