IEC61850 报告上送与 DataModel 并发读取崩溃修复
版本:1.0
日期:2026-07-04
分类:Reports / DataModel / MMS 连接 / 并发安全 / ICD 导出回归 / Bug 修复
状态:已实施
1. 问题现象
IEC61850 客户端使能 BRCB 或 URCB 后,设备会在 libIEC61850 的接收线程中主动上送报告。此时如果用户同时进入 DataModel 页面、重新发现模型或读取 DataSet,旧实现可能让报告回调和同步 MMS 请求竞争同一个原生 IedConnection,严重时整个 Python 进程直接退出,无法由常规异常处理捕获。
该问题具有明显的并发特征:单独读取 DataModel 或单独订阅报告通常正常,只有报告持续上送期间执行模型读取才容易触发。
2. 根因分类
| 分类 | 根因 | 风险 |
|---|---|---|
| 连接复用 | 报告接收、RCB 配置、模型发现和测点读取共用同一个 MMS association | 同步请求与异步回调互相阻塞或重入 |
| GIL 协作 | 部分阻塞式 SWIG 原生调用未优先使用释放 GIL 的包装入口 | 原生接收线程等待进入 Python 回调时可能形成僵持 |
| 生命周期 | 断线或重连时可能在报告回调尚未退出前销毁原生连接 | 回调继续访问失效句柄,导致进程级崩溃 |
| 回调作用域 | 回调注册表按进程管理,清理时缺少 connection 归属过滤 | 一条连接重连可能误清理其他连接或客户端的报告 |
| 长操作保护 | 完整模型发现未覆盖整个原生连接生命周期锁 | 发现过程中可能被断线或重连销毁底层句柄 |
3. 修复方案
3.1 双 MMS 连接隔离
客户端现在为同一台 IED 建立两个彼此独立的 association:
| 连接 | 职责 |
|---|---|
| 主连接 | 模型发现、DataModel、DataSet、单点读写和元数据读取 |
| 报告连接 | RCB 参数读写、报告使能、GI 和主动报告接收 |
报告连接建立失败不会破坏已成功建立的主连接,Reports 插件在实际使用前会按需重连。主连接完成模型发现后,会把逻辑设备列表和模型前缀同步给报告连接,保证 RCB 引用解析一致。
这种拆分从 association 层消除了“同步模型读取占用连接时,报告接收线程又要在同一连接上进入 Python”的冲突。
3.2 原生阻塞调用统一释放 GIL
新增 call_gil_safe() 入口。对于 pyiec61850-ng 已提供的 pyWrap_* 方法,优先调用会释放 GIL 的包装函数;旧版绑定不存在包装函数时,再兼容回退到原始方法。
当前已覆盖以下高风险路径:
- DataModel、DataSet 和普通测点的
readObject; - 元数据读取;
- RCB 的
getRCBValues、setRCBValues; - 模型发现期间的运行时值读取。
3.3 安全断开顺序
断开客户端时严格执行以下顺序:
- 对当前报告连接上的活跃 RCB 写入
RptEna=false; - 给已到达接收线程的最后一批报告留出处理窗口;
- 等待已经进入 Python 的报告回调退出;
- 注销当前 association 的回调和订阅者;
- 先断开报告连接,再断开主连接。
回调处理器会按 connection 记录正在执行的回调数量,并通过条件变量等待排空。即使等待超时,也会输出明确日志,避免无声挂起。
3.4 回调按连接隔离
每条回调注册信息新增所属 connection。查询活跃报告、复用相同 RptId 的订阅者以及批量清理回调时,都只作用于指定 association。
普通数据连接重连不再全局执行 shutdown_all(),从而避免误删独立报告连接或另一个客户端实例的订阅。
3.5 模型发现生命周期保护
完整在线模型发现和远程刷新现在都在 native_operation() 保护区内执行。只要发现尚未结束,断线或重连就不能销毁同一个原生连接句柄。DataModel 插件的 LN、DO、DA/BDA 浏览和描述读取也复用同一保护机制。
4. ICD/XML 导出回归分类
本次并发修复扩大回归范围后,完整测试曾出现 23 项 ICD/XML 导出失败,但同一组测试单独运行时全部通过。排查确认,失败并非 23 个独立的导出算法缺陷,而是两个测试文件仍从 proto.* 导入模块;在安装过旧版项目包的虚拟环境中,这会加载 site-packages/proto 的旧代码,而业务和其他测试使用的是工作区内的 src.proto.*。
相关测试已统一改为 src.proto.* 规范导入,并删除了错误拼接到 src/src 的 sys.path 修改。这样无论单独运行还是整目录运行,测试都只验证当前工作区代码,不再受旧安装包和执行顺序影响。
导出断言按以下类别继续保留:
- IED 名称推断与 LD 实例名拆分;
- FCDA 的 FC、前缀和空属性规范化;
- DOType、DAType、EnumType 生成与去重;
- q、t、origin、setMag 等 CDC/结构类型映射;
- BRCB/URCB 实例聚合和
RptEnabled max; - 标准 SCL 与自定义
ServerModelXML 两种导出格式。
ICD/XML 导出器专项共 75 项测试。该组问题与双连接实现没有运行时耦合,但被纳入同一次 IEC61850 回归,防止连接修复掩盖模型导出兼容性问题。
5. 兼容性说明
IEC61850Client的公开连接、读取、写入和报告 API 保持不变;is_connected仍表示主连接状态;- 报告连接可独立恢复,不影响主连接上的模型浏览和测点操作;
- 不支持
pyWrap_*的旧版绑定仍可通过原生函数兼容运行; - 报告回调缓存、GI 路由和前端报告数据格式保持不变。
6. 验证重点
新增回归测试覆盖:
- 主连接与报告连接不是同一对象;
- 连接顺序为主连接后报告连接;
- 断开顺序为报告排空、报告断开、主连接断开;
- GIL-safe 包装函数优先于原始原生调用;
- 一条连接的
shutdown_all()不影响其他连接; - RCB 禁用发生在回调等待和订阅注销之前。
6.1 自动化回归
| 验证项 | 结果 |
|---|---|
| IEC61850 非设备依赖测试 | 169 项通过 |
| ICD/XML 多格式导出专项 | 75 项通过,整套执行不再加载旧安装包 |
| Ruff 与 Python 编译检查 | 通过 |
| VitePress 文档构建 | 通过 |
6.2 真实 MMS 冒烟
在本机 127.0.0.1:102 的真实 MMS 服务上完成双连接冒烟:
- 主连接与报告连接均建立成功,两个底层连接对象彼此独立;
- 模型发现完成:25 个 LD、59 个 LN、4566 个 DO、18252 个 DA、34 个 DataSet、272 个 RCB;
- 模型发现后报告连接仍保持可用;
- 客户端断开后,主连接和报告连接状态均为未连接;
- 整个建立、发现和安全断开过程无崩溃、无回调残留。
后续现场长稳验证应继续覆盖“RCB 持续主动上送期间反复读取 DataModel/DataSet”的压力场景,以验证不同厂商 IED 和网络延迟下的长期表现。
7. 结果
修复后,报告主动上送与 DataModel/DataSet 同步读取分别运行在独立 MMS association 上;阻塞式原生调用能够让出 GIL;断线前会先停止报告并排空 Python 回调。原有最危险的同连接重入和失效句柄访问路径已被切断。