OpenTAP 资源依赖调度:ResourceOpenBehavior 如何避免循环引用卡死

背景

OpenTAP 的资源(Instrument、DUT、ResultListener)在测试执行前需要自动打开,但真实工程里资源之间常常互相引用:A 依赖 B,B 又可能间接依赖 A。若全都按“先依赖后打开”的串行规则处理,很容易在循环依赖场景里陷入等待链。OpenTAP 的做法不是简单报错,而是通过 ResourceOpenBehavior 把依赖分成强依赖与弱依赖,在可并行处主动并行,降低死锁风险。

框架分析

这套机制分两层:

  1. 依赖分析层ResourceDependencyAnalyzer 扫描资源属性,读取 [ResourceOpen(...)] 特性。默认是 Before(强依赖),InParallel 会进入弱依赖,Ignore 则直接跳过。
  2. 调度执行层ResourceTaskManager 为每个资源创建异步打开任务。强依赖通过 Task.WaitAll 保证顺序;弱依赖不阻塞当前 Open(),而是在后续 finallyTasks 中等待后再触发 ResourceOpened 事件。

这个分层很关键:分析层决定图结构,调度层决定等待策略,职责清晰,调试时也容易定位到底是“依赖声明问题”还是“线程等待问题”。

实现过程

执行入口在 BeginStep(..., TestPlanExecutionStage.Open/Execute, ...),它先汇总 StaticResources + EnabledSteps,再调用 BeginOpenResources 批量拉起任务。每个资源的 OpenResource 逻辑是:先等强依赖完成,再执行 Resource.Open(),最后处理弱依赖完成后的回调。

可复现实验(验证循环依赖在并行标注下可运行):

1
2
cd /home/ops/clawd/repos/opentap
dotnet test Engine.UnitTests/Engine.UnitTests.csproj --filter "FullyQualifiedName~ResourceDependencyTests.CircularResourceReference"

该用例在 parallel=trueparallel=false 下分别断言不同 Verdict,正好对应 InParallel 对循环引用行为的影响。

注意事项

  1. InParallel 只适合“打开阶段不要求对方已完全可用”的依赖;若 Open() 内必须立刻访问对端状态,仍应使用默认强依赖。
  2. Ignore 会让引用资源不参与自动开关,适合手工生命周期管理,但也最容易造成“看起来配置了资源却没被打开”的误判。
  3. 弱依赖的等待被放到后置任务中,是为避免循环等待;若你在 ResourceOpened 里做重操作,仍可能拉长整体启动时间。
  4. 资源从设置中被删除但仍被引用时,BeginOpenResources 会直接抛错,别把它当成普通空指针处理。

小结

ResourceOpenBehavior 的价值不在“多一个枚举”,而在于它把资源依赖从单一拓扑排序升级成“强约束 + 弱约束”的混合调度模型。对复杂仪器链路来说,这比一味串行更稳,也比全并行更可控。实战里建议先用默认 Before,只对确认安全的环路边标注 InParallel,这样最不容易踩坑。

关键源码路径:

  • Engine/ResourceTaskManager.cs
  • Engine/ResourceDependencyAnalyzer.cs
  • Engine.UnitTests/ResourceDependencyTests.cs
  • Engine/TestPlanRun.cs