Skip to content

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 的 getRCBValuessetRCBValues
  • 模型发现期间的运行时值读取。

3.3 安全断开顺序

断开客户端时严格执行以下顺序:

  1. 对当前报告连接上的活跃 RCB 写入 RptEna=false
  2. 给已到达接收线程的最后一批报告留出处理窗口;
  3. 等待已经进入 Python 的报告回调退出;
  4. 注销当前 association 的回调和订阅者;
  5. 先断开报告连接,再断开主连接。

回调处理器会按 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/srcsys.path 修改。这样无论单独运行还是整目录运行,测试都只验证当前工作区代码,不再受旧安装包和执行顺序影响。

导出断言按以下类别继续保留:

  • IED 名称推断与 LD 实例名拆分;
  • FCDA 的 FC、前缀和空属性规范化;
  • DOType、DAType、EnumType 生成与去重;
  • q、t、origin、setMag 等 CDC/结构类型映射;
  • BRCB/URCB 实例聚合和 RptEnabled max
  • 标准 SCL 与自定义 ServerModel XML 两种导出格式。

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 回调。原有最危险的同连接重入和失效句柄访问路径已被切断。

Released under the Apache 2.0 License.