ResourceOwnerToken(含 Generation 世代号)、reservation_ref 字段、IAutoDeviceLease 作用域——这些概念与工业界标准的"租约(Lease)+ 栅栏令牌(Fencing Token)"模型一一对应。唯一的问题是:整套机制活在内存里。
| 组件 | 文件 | 现状 | 跨进程能力 |
|---|---|---|---|
| ResourceManager | Aging.Infrastructure/Core/ResourceManager.cs | ConcurrentDictionary 存预约 + SemaphoreSlim(1,1) 每资源细锁;Generation 全局自增 |
❌ 纯进程内存,其他进程完全不可见 |
| ResourceOwnerToken | Contracts/ResourceOwnerToken.cs | TokenId + OwnerType(Production/Maintenance) + Generation + IssuedAt/ExpiresAt | ⚠️ 结构可序列化,但目前没有跨进程校验路径 |
| AutoDeviceFacade / Lease | Services/AutoDeviceFacade.cs | Reserve → DeviceService 分派 → finally Release;Lease 作用域复用 | ❌ 依赖内存版 ResourceManager |
| DeviceService | Services/DeviceService.cs | 执行前 ValidateOwner(内存查表)→ AcquireOperationAsync → driver 下发 |
❌ 校验仅本进程有效 |
| DebugSessionManager | Plugin.DeviceDebug/Model/DebugSessionManager.cs | RequestMaintenance 已捕获 ResourceConflictException,并把 reservation.ReservationId 写入 session.ReservationRef 持久化 |
⚠️ 已预留挂接点,差一个"活的"仲裁源 |
| debug_session 表 | Plugin.DeviceDebug/Model/DeviceDebugModel.cs | 有 reservation_ref TEXT、state、closed_at,无心跳/过期列 |
⚠️ 可扩展 |
| IPC 设施 | ConnectionUtil/PipeServer.cs · PipeClient.cs | 命名管道(Message 模式)已实现:多实例服务端、连接事件、请求-响应 SendMessage(带超时) | ✅ 可直接作为 Device Broker 的传输层,无需新造轮子 |
| OpenTAP SynchronizationManager | OpenTAP 9.31 引擎 | 进程内 RequestLock(resource, timeout) 按仪器互斥 |
❌ 官方文档明确:仅进程内 |
open;网口仪器则允许同机多连接但需要"逻辑独占"语义。→ 需要把连接持有者与租约持有者绑定,必要时引入代理进程。| 维度 | 场景 A:进程内多线程 | 场景 B:跨进程/跨机器 |
|---|---|---|
| 典型触发 | TestPlan 中 Parallel 步骤 / MultiSite 同时跑不同分支 | 多工位独立进程、HMI 与自动测试并行、CI 节点 |
| 互斥原语 | SemaphoreSlim / lock / SynchronizationManager.RequestLock | OS 命名内核对象 / DB 租约 / Broker 队列 |
| 崩溃表现 | 进程死 → 锁随进程消失(内核回收) | 持有者死亡后锁残留,必须靠 TTL/看门狗回收 |
| 可见性 | 天然可见(同一堆) | 必须外部化状态(DB / Broker / 共享内存) |
| 你们的现状 | 已解决(ResourceManager 内存版即是干这个的) | 完全未解决——本次方案的主战场 |
LayeredResourceManager,对上层 IAutoDeviceFacade 完全透明。SynchronizationManager。
StackOverflow/原生驱动崩溃拖死全部 N 个工位;老化工龄动辄几十小时,进程级隔离是刚需。内存泄漏在多进程下可以按工位轮换回收,单进程无解。
new Mutex(false, "Global\\Lease_CAN-001"),跨进程抢内核对象。
AbandonedMutexException 时上一个持有者操作到哪一步了完全不可知,对"人在接线/输出已打开"这类安全状态是盲区;debug_operation 审计体系脱节;Global\ 命名在 Windows 服务/会话隔离下还有权限坑。data.db 新表 device_lease,用 BEGIN IMMEDIATE 事务实现跨进程原子的"比较并换入"(CAS),TTL 过期即被视为崩溃可回收,Generation 作栅栏令牌防"僵尸持有者"。
DbLeaseStore 内部类,ResourceManager 升级为组合模式:内存字典继续做本进程快速路径与信号量互斥,预约/校验同时读写 DB 表。上层 AutoDeviceFacade / DeviceService / DebugSessionManager / IAutoDeviceLease 零改动,只换 Bootstrapper 里的注册实例。
DeviceBroker.exe,所有物理连接(COM/USB/CAN 卡句柄、SCPI TCP socket)只由它持有;各工位进程通过命名管道把命令转发给 Broker 排队执行。仓库现成的 ConnectionUtil/PipeServer·PipeClient 即可做传输层。
CreateFile。若仅靠租约,每次租约易手都要 close→open 一次,USB 重枚举可达秒级且丢缓冲;Broker 让连接永不换手,切换的只是"逻辑所有权";| OpenTAP 能力 | 是什么 | 能否解决跨进程仲裁 |
|---|---|---|
Instrument : Resource 资源模型 | 仪器生命周期托管 + 编辑器绑定 | ❌ 实例生命周期仅进程内;不感知其他进程 |
SynchronizationManager | 进程内按仪器互斥锁 | ❌ 设计目标就是单进程 |
ResultProxy / 远程执行(tap run --remote、Session.Local) | 把一个 TestPlan 切分到远端执行节点,结果回流 | ⚠️ 解决"执行分布",不解决"两台机抢一台仪器"——每个远端 session 各自还是单进程视野 |
| BenchStore / ResourceSettings(9.23+) | 台架资源配置持久化(XML) | ⚠️ 配置可以共享,仲裁状态不行(无 TTL/心跳概念) |
| 私有包仓库分发 | 驱动以 .opentap 包安装 | ✅ 解决驱动分发(与并发仲裁正交,值得并行推进) |
IResourcePlan/ResourceConflicts(9.20+) | 资源规划/冲突静态检查(编辑器里提示"两个序列用了同一仪器") | ⚠️ 只能提前发现冲突,运行时多进程并发仍不仲裁 |
| 维度 | ①单进程收敛 | ②Named Mutex | ③DB 租约表 ⭐ | ④Device Broker | ⑤OpenTAP 现成 |
|---|---|---|---|---|---|
| 并发安全(跨进程) | 伪命题(无多进程) | 互斥可以,但无状态语义 | 强一致(DB 事务 CAS) | 单点串行化,最强 | 不支持 |
| 崩溃残留处理 | 进程死=锁消失 | Abandoned 可感知但盲接 | TTL 自动过期 + 回收 | Broker 死=全停(需看门狗) | — |
| 优先级/抢占/优雅交接 | 代码可实现 | 表达不了 | 状态列+grace 字段原生支持 | 队列策略任意定制 | 无 |
| 审计可追溯 | 无 | 无 | 表即历史(可挂 debug_operation) | Broker 全权日志 | ResultProxy 有结果无仲裁 |
| 性能开销 | 纳秒级 | 微秒级 | 毫秒级(WAL 点写) | 毫秒级(IPC 一跳) | — |
| 实现成本 | 0(重构架构为负) | 0.5 人日 | 3~4 人日 | 5~8 人日 | 已内建/不适用 |
| 可扩展到多机 | 否 | 否 | 需换服务端 DB | 是(Broker=网关) | 部分 |
| 物理句柄独占问题 | 天然规避 | 不解决(close/open 抖动) | 租约期内保连接可缓解 | 根治(唯一持有者) | 不解决 |
| 调试难度 | 低 | 内核对象黑盒 | 低(一张表全透明,SELECT 即现场) | 需双进程联调 | — |
| 与现有代码衔接 | 大改 | 需新增旁路 | IResourceManager 实现层替换,上层零改 | Driver 层拆分转发端/宿主端 | 正交复用 |
| 层 | 解决什么 | 何时启用 |
|---|---|---|
| 内存层(已有) | 同进程多线程细锁、快速失败 | 始终(零成本) |
| DB 租约层(新增) | 跨进程运行时仲裁、优先级抢占、审计、崩溃回收 | Phase 1 —— 主体投入 |
| Broker 层(新增) | 物理句柄独占、多机共享、命令队列 | Phase 3 —— 仅当出现:① 串口/USB 重开成本不可接受 ② 多机共测一台机上的设备 ③ 分时复用设备要排队 |
| OpenTAP 生态 | 驱动分发(包)、配置共享(Bench 思路) | 与 Phase 1/3 正交并行推进 |
BEGIN IMMEDIATE; -- 拿写锁:跨进程串行化入口(SQLite WAL 下读写不互斥,写写排队)
-- ① 顺带回收所有已过期租约(懒回收,无需独立守护也能自愈)
DELETE FROM device_lease
WHERE resource_key = @rk AND expires_at < datetime('now');
-- ② 检查当前有效占用
SELECT owner_id, owner_type, state, grace_until INTO @cur FROM device_lease WHERE resource_key = @rk;
-- ③ 无占用 → 插入新租约(generation 取全局序)
INSERT INTO device_lease(resource_key, owner_id, owner_type, token_id, generation,
machine_name, host_process_id, state, acquired_at, heartbeat_at, expires_at)
VALUES(@rk, @owner, @otype, @token, @gen, @machine, @pid, 0,
datetime('now'), datetime('now'), datetime('now', '+30 seconds'));
-- ④ 有占用且请求者是 Maintenance → 不直接抢,标记让渡窗口
UPDATE device_lease SET state = 1, grace_until = datetime('now','+30 seconds')
WHERE resource_key = @rk AND state = 0;
COMMIT;
// AutoDeviceLease 内部:TTL/3 周期定时器(Phase 1 即实现)
var affected = db.ExecuteNonQuery(@"
UPDATE device_lease
SET heartbeat_at = datetime('now'),
expires_at = datetime('now', '+30 seconds'),
state = CASE WHEN state = 1 THEN state ELSE 0 END -- 保留让渡标记
WHERE resource_key = @rk AND token_id = @token AND generation = @gen");
if (affected == 0) {
// 行不在了或 generation 已被超越 ⇒ 我已被抢占/回收 —— 本地立即熔断:
// 1) 停止下发任何命令 2) 释放物理连接 3) 会话标记 RecoveryRequired 4) 上报一次告警
OnLeaseLost();
}
if (db.ReadState() == PendingHandover && InStepBoundary) {
Dispose(); // 优雅让渡:只在 TestStep 边界释放,绝不在一条命令中间放手
}
datetime('now')(DB 时钟),客户端时间仅作参考列;② 每条命令下发前必须 FenceCheck(一行点查,成本≈1ms);③ 让渡/释放只发生在 TestStep 边界,保证设备不处于"半条指令"状态。目标场景:工程师正打开机柜接 CAN 线(维护模式),全自动程序绝不能突然下发 OUTP ON。规则明确定义如下(策略引擎集中实现于新增 Aging.Infrastructure/Core/LeaseArbiter.cs):
| 请求:Production(全自动) | 请求:Maintenance(手动维护) | 请求:ReadOnly(监控/看数据) | |
|---|---|---|---|
| 持有:Production | 排队(FIFO 或按工位优先级,带超时) | 让渡流程:PendingHandover + grace 30s;超时未让渡 → 允许强占 + 旧持有者栅栏失效 | 放行(不建租约,走旁路只读) |
| 持有:Maintenance | 直接拒绝(ResourceConflictException,"有人在维护";UI 弹窗告知工程师) | 拒绝(第二个人也不许碰——一台设备同时只允许一个维护会话,复用现有 OpenSession 重复检查逻辑) | 放行 |
| 持有:ReadOnly | 立即放行(读锁不构成障碍) | 立即放行 | 放行 |
ReserveAsync + 捕获 ResourceConflictException → 只需把内存版换成 Layered 版即自动获得跨进程拒绝;ValidateCommand() 已有"MaintenanceActive + ValidateOwner"双重检查,把 ValidateOwner 改为查 DB 栅栏,就得到"人在接线时程序连命令都发不出"的硬保证——即使自动程序逻辑有 bug。UPDATE debug_session SET state=Closed(现有逻辑保持);grace 内未完成的步骤,策略改为"拒绝进入,已运行不打断"(可配置:安全类工位选择强占+设备紧急安全态;产能类工位选择排队等待);device_lease + debug_session,一条 JOIN 即可)。device_lease 表本身就是"占用历史"(Release 时改写 released_at 再保留 7 天,或双表:当前 + 归档);DeviceService 的命令继续写 debug_operation,新增两列 owner_type, lease_generation,即可回答"这条 OUTP ON 是哪个工位、哪一代持有者发的"——事故追溯闭环。| 异常场景 | 成因 | 对策 | 机制落点 |
|---|---|---|---|
| 多设备死锁(工位A锁{电源}等{CAN},B反序) | 组申请加锁顺序不一致 | ① 全局强制按 resource_key 字典序申请(AcquireGroupAsync 内部排序)② 事务内"全有或全无":任一冲突则整体回滚重试(wait-die:新请求者退避重试,老持有者不被饿死) ③ 组申请总超时(默认 60s) |
AutoDeviceFacade 新增 AcquireGroupAsync |
| 进程崩溃 → 孤儿锁 | kill / BSOD / 断电,Release 没跑 | ① TTL 30s 自动过期(任何 Acquire 顺带 DELETE 过期行 = 懒回收) ② 同机看门狗:lease 记录 machine_name + host_process_id,本机新进程启动时枚举存活 PID 提前回收(不用等 TTL)③ HMI"强制释放"按钮(需 ADMIN 角色,写审计 note="force_release") |
device_lease 列 + Bootstrapper 启动自检 |
| 僵尸持有者(网络分区/GC停顿后复活继续发命令) | 进程活着但被抢占过 | 栅栏令牌:命令必须携带 (token_id, generation),DB 中 generation 已变 → 驱动入口直接拒绝(§5.2 时序 15-17 步)。这是比 TTL 更根本的安全网——TTL 管"谁能拿",栅栏管"拿过的人过期后不能碰"。 |
DeviceService.ExecuteCommandAsync 入口 FenceCheck |
| 时钟不一致(多机 NTP 漂移) | expires 用本地时间比较会误判 | ① 全部时间判定用 DB 侧时钟(datetime('now')),客户端只传相对 TTL("+30 seconds"),从不传绝对时间② Phase 3 Broker 化后统一用 Broker 时钟 ③ 多机 + 共享 DB 场景要求 NTP(运维项,误差<1s 即可) |
SQL 写法约束(§5.3 全部合规) |
| 心跳线程被 UI 卡顿饿死 | WPF 主线程阻塞 / 进程假死 | 心跳用独立后台线程(非 UI Dispatcher),且心跳失败连续 2 次即本地熔断——宁可误停不可漏停 | AutoDeviceLease 内 Timer 实现 |
| 重试不幂等(超时后重发"OUTP ON"是安全的,重发"计数器+1"不安全) | 网络/DB 抖动自动重试 | ① 命令分级:设备层可安全重发(设置型 SET xx=yy ✅ 幂等;累加/触发型 ⚠️ 禁止自动重试,上抛) ② debug_operation.operation_id 已是客户端生成 GUID → 作为幂等键 INSERT OR IGNORE,重复到达不二次执行③ 老化场景的 SCPI/CAN 写命令多为设置型,天然幂等友好,但必须在驱动接口上用 CommandSemantics.Idempotent 显式声明 |
DeviceCommandResult + 驱动装饰器 |
| SQLite 写竞争(N 个工位 × 心跳+申请集中写一行区域) | 写锁串行化 | WAL 模式 + busy_timeout=5000 + 事务只包住 2~3 条点写(<5ms);老化命令节奏(秒级)下并发写压力可忽略。真到高频(>100 写/秒)再迁服务端 DB | 连接串 PRAGMA(见 §9 DbLeaseStore) |
| 抢占风暴(两个高优请求同时到达) | 并发 PendingHandover 标记 | 仲裁在 BEGIN IMMEDIATE 事务内串行判定;第二个维护请求会看到"已有维护排队"而被告知排队位置,不产生双 grace 竞争 |
事务天然覆盖 |
| 模式 | 语义 | 租约形态 | 典型设备(本项目) |
|---|---|---|---|
Exclusive 独占 |
一个工位全程持有,测试期间别人碰不到 | 整步骤级长租约(TTL 靠心跳维持数小时) | 工位专用电源、DUT 专用 CAN 卡通道 |
TimeShared 分时串行 |
多条命令排队过设备,每条原子 | 命令级短租约(秒级)或 Broker 队列 | 共享 DAQ 读数、公用万用表 |
SharedRead 共享读+独占写 |
监控随便读(旁路),写必须抢租约 | 读不走仲裁;写=Exclusive 租约 | 温箱(状态人人可看,设温必须独占)、CAN 总线监听 |
DeviceDefinition.access_mode 列,注册页下拉选择)Exclusive;TimeShared(串行队列);SharedRead。SlotId。工位进程启动参数带 SlotId → 组租约自动展开为该 Slot 绑定的全部设备 resource_key 列表(按字典序申请);AcquireGroupAsync(groupKey, ...):查 device_group(或即时 JOIN SlotDeviceMapping)→ 排序 → 单事务逐台 INSERT,任一冲突整体回滚 → 避免"占了一半"的半死状态;CHAMBER-01 这类跨 Slot 设备在 DeviceDefinition.access_mode 标 TimeShared/SharedRead,组申请时跳过,命令执行时才抢短租约;CAN_CARD01:CH2——ResourceKey 本来就支持 Channel(见 Contracts/ResourceKey.cs),CAN 帧 ID 级再细分可用 resource_type='CAN_FRAME' + channel=帧段(Broker 阶段实现命令级串行化时够用)。-- ① 跨进程租约表(核心新表)
CREATE TABLE IF NOT EXISTS device_lease (
resource_key TEXT PRIMARY KEY, -- 'CAN-001:Generic' 与 ResourceKey.ToString() 一致
owner_id TEXT NOT NULL, -- 'Station3.Run20261103' 工位可读标识
owner_type INTEGER NOT NULL, -- 0=Production 1=Maintenance 2=ReadOnly
token_id TEXT NOT NULL, -- ResourceOwnerToken.TokenId
generation INTEGER NOT NULL, -- 栅栏令牌:每次成功申请 +1(全局取 max+1)
machine_name TEXT NOT NULL DEFAULT '',
host_process_id INTEGER NOT NULL DEFAULT 0,
slot_id TEXT, -- 关联工位 Slot(组审计用)
state INTEGER NOT NULL DEFAULT 0, -- 0=Active 1=PendingHandover
acquired_at TEXT NOT NULL,
heartbeat_at TEXT NOT NULL,
expires_at TEXT NOT NULL, -- 全部由 DB datetime('now') 生成
grace_until TEXT,
note TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_lease_expire ON device_lease(expires_at);
-- ② 设备访问模式(现有表加列,老库幂等迁移)
ALTER TABLE DeviceDefinition ADD COLUMN access_mode INTEGER NOT NULL DEFAULT 0;
-- 0=Exclusive 1=TimeShared 2=SharedRead
-- ③ 工位设备组(组租约的展开依据;也可不建表,运行时 JOIN SlotDeviceMapping)
CREATE TABLE IF NOT EXISTS device_group (
group_key TEXT PRIMARY KEY, -- = SlotId 或自定义
resource_key TEXT NOT NULL,
order_hint INTEGER DEFAULT 0,
PRIMARY KEY (group_key, resource_key)
);
-- ④ debug_session 加 3 列(会话↔租约联动可视化)
ALTER TABLE debug_session ADD COLUMN lease_generation INTEGER;
ALTER TABLE debug_session ADD COLUMN lease_owner_type INTEGER;
ALTER TABLE debug_session ADD COLUMN heartbeat_at TEXT;
-- ⑤ debug_operation 加 2 列(命令级追溯)
ALTER TABLE debug_operation ADD COLUMN lease_generation INTEGER;
ALTER TABLE debug_operation ADD COLUMN idempotency_key TEXT; -- 幂等重试去重
CREATE UNIQUE INDEX IF NOT EXISTS idx_op_dedup ON debug_operation(idempotency_key) WHERE idempotency_key != '';
| 文件 | 改动类型 | 内容 |
|---|---|---|
| Aging.Infrastructure/Contracts/IResourceManager.cs | 扩展(向后兼容) | 新增 3 个方法,默认实现委托现有语义:Task<bool> RenewAsync(ResourceOwnerToken, ResourceKey, int ttlSeconds, CancellationToken)(bool valid, string? holderInfo, int? graceSeconds) ProbeConflict(ResourceKey) // UI 显示"被谁占用"Task<GroupLeaseHandle> AcquireGroupAsync(string groupKey, ResourceOwnerToken owner, CancellationToken ct) |
| Aging.Infrastructure/Core/DbLeaseStore.cs ★新建 | 新增 | §5.3/§5.4 的 SQL 封装;WAL/busy_timeout PRAGMA;懒回收;同机 PID 看门狗。约 250 行 |
| Aging.Infrastructure/Core/ResourceManager.cs | 改造为组合 | 保留内存字典 + SemaphoreSlim(快速路径);预约/校验转发 DbLeaseStore;离线模式(DB 不可达)降级为纯内存 + 每 5s 重连并在重连后栅栏自检 |
| Aging.Infrastructure/Core/LeaseArbiter.cs ★新建 | 新增 | §6.1 优先级矩阵的策略引擎:让渡/拒绝/排队判定,策略参数(grace、抢占开关)进配置 |
| Aging.Infrastructure/Services/AutoDeviceFacade.cs | 增强 | ① ReserveCoreAsync 的 owner 标识从随机 Guid 改为可配置 StationGlobals.StationId(多机/多工位可读性);② AutoDeviceLease 构造函数启动后台心跳 Timer(TTL/3),Dispose 停止;③ 捕获 PendingHandover → TryYieldAtBoundary() |
| Aging.Infrastructure/Services/DeviceService.cs | 增强 | ValidateOwner 升级为 DB 栅栏校验(查 generation 匹配);不匹配返回 ErrorCode="LEASE_STALE" 结构化失败,不抛裸异常 |
| Aging.Infrastructure/Drivers/DriverFactory.cs + 各驱动 | Phase 3 | 新增 transport=="CAN-REMOTE" 等代理型驱动(把命令序列化为 Broker 请求);现有直连驱动不动。IBrokeredTransport 接口定义于 Contracts |
| Plugin.DeviceDebug/Model/DebugSessionManager.cs | 微调 | RequestMaintenance 处理 PendingHandover(转异步等待 + UI 回调"等待工位让渡,剩余 X 秒");冲突异常 message 已含占用者 → 直接给 ToastHelper |
| Plugin.DeviceDebug/Model/DeviceDebugModel.cs | 微调 | debug_session 新增列写入;启动时 SELECT ... WHERE state NOT IN (Closed) AND heartbeat过期 做孤儿会话回收(置 Faulted) |
| Plugin.DeviceConfig/Model/DeviceConfigModel.cs | DDL | §9.1 的建表/加列(幂等,IF NOT EXISTS / try-catch ALTER);DeviceEditModal.xaml(.cs) 增加 access_mode 下拉 |
| Aging.Infrastructure/InfrastructureBootstrapper.cs | 配置点 | 注册 LayeredResourceManager(带 dbPath)替代纯内存版;读 StationId;可选注册 BrokerClient 传输 |
| ★新建 DeviceBroker/ 控制台工程(Phase 3) | 新增 | 引用 ConnectionUtil 现成 PipeServer;协议:{op, deviceId, channel, payload, token, gen, idemKey};内部按设备队列 + 复用 DriverFactory 创建真实驱动;崩溃看门狗(父进程句柄失联自动退出交 HMI 拉起) |
// DbLeaseStore —— 原子申请(对应 §5.3 SQL)
public sealed class DbLeaseStore
{
public DbLeaseStore(string dataDbPath) { /* PRAGMA journal_mode=WAL; busy_timeout=5000 */ }
public enum AcquireResult { Acquired, Renewed, QueuedHandover, RejectedMaintenanceHeld, RejectedByProductionHeld }
public AcquireResult TryAcquire(string resourceKey, ResourceOwnerToken owner, OwnerType type,
string machine, int pid, string? slotId, int ttlSeconds);
public bool Renew(string resourceKey, string tokenId, long generation, int ttlSeconds);
public bool Release(string resourceKey, string tokenId);
public long CurrentGeneration(string resourceKey); // FenceCheck 用
public int SweepExpired(); // 懒回收兜底
public (bool valid, string? holder, int? graceSec) ProbeConflict(string resourceKey);
}
// AutoDeviceLease 心跳(Phase 1)——独立线程,UI 卡顿不影响
_beacon = new Timer(_ => {
try {
if (!_store.Renew(DeviceId, _owner.TokenId, _owner.Generation, _ttl))
{ LeaseLost?.Invoke(this, EventArgs.Empty); return; }
if (_store.IsPendingHandover(DeviceId)) _handoverTcs.TrySetResult(true);
_failCount = 0;
} catch { if (++_failCount >= 2) LeaseLost?.Invoke(this, EventArgs.Empty); }
}, null, _ttlMs/3, _ttlMs/3);
// TestStep 基类在步骤边界让渡(集成测试演示用法)
if (lease.HandoverRequested) lease.Dispose(); // 步骤结束点检查,绝不在命令中途放手
| 用例 | 验证点 |
|---|---|
| Acquire 并发 20 线程同 key | 恰好 1 个 Acquired,其余 Rejected/Renewed(同 token);generation 严格 +1 无跳变竞争 |
| Renew 用旧 generation | 返回 false(栅栏语义) |
| TTL 过期 → 直接 Acquire 过期持有者的 key | 懒回收生效:先 DELETE 过期行再插入成功 |
| 仲裁矩阵全遍历(3×3 持有×请求) | §6.1 每个格子一个断言 |
| 组申请部分冲突 | 全回滚,不留下"占了一半"的行 |
// 伪代码:主进程 fork N 个自身 --station-worker 模式的子进程
for (int s = 0; s < 4; s++)
Process.Start(exe, $"--worker {s} --device CAN-001 --loops 200");
// 子 worker 逻辑:循环 [TryAcquire → 发一帧 → 校验 → Dispose],统计成功轮转次数
// 主进程断言:① 任意时刻 device_lease 只有 1 行有效 ② 所有帧的 generation 单调 ③ 无双写
// ④ 中途 Kill(随机 worker) → 其余 worker ≤ TTL+5s 内接管,测试继续收敛为 PASS
额外两个杀手级用例:
Process.Kill 持有者(不跑 Dispose)→ 断言 ≤35s 后被其他工位自动接管;host_process_id 同机看门狗路径则 ≤2s 回收;LEASE_STALE。| 阶段 | 交付物 | 解决的痛点 | 工作量 |
|---|---|---|---|
| Phase 1 跨进程租约 |
device_lease DDL;DbLeaseStore;ResourceManager 组合化;心跳 + 栅栏 + 懒回收;Bootstrapper 切换;多进程集成测试 |
多进程互斥仲裁 ✅ 崩溃自愈 ✅ 审计可追溯 ✅ 上层零改动 ✅ | 3~4 人日 |
| Phase 2 仲裁规则 & UI |
LeaseArbiter 优先级矩阵;维护抢占/让渡流程;DebugSessionManager PendingHandover;HMI"占用者信息/强制释放";access_mode 注册页 + 组租约 AcquireGroupAsync |
"人在接线程序不能动"硬保证 ✅ 工位分组 ✅ | 2~3 人日 |
| Phase 3 Device Broker(按需触发) |
DeviceBroker 工程(复用 ConnectionUtil 管道);代理型驱动 *BrokeredDriver;多机接入;命令队列 + TimeShared 策略落地 |
串口/USB 句柄独占根治 ✅ 多机共享 ✅ 分时命令串行化 ✅ | 5~8 人日 |
| Phase 4 演进(可选) |
驱动迁移为 OpenTAP Instrument 插件包(.opentap);Bench 配置与 DeviceConfig 单源化;HMI 内嵌驱动管理(Image API) | 厂商驱动生态复用、分发运维 | 另评估 |
| 反模式 | 后果 | 正确做法 |
|---|---|---|
| 多进程各自 open 同一 COM 口/CAN 卡句柄"碰运气" | WriteFile 交错、数据撕裂、蓝屏级未定义行为 | 要么租约保证单持有者(Phase 1),要么 Broker 唯一持有(Phase 3)。物理层不允许并发是硬约束,任何软件方案不能挑战 |
| 仅用 Named Mutex 当租约 | 崩溃后接管方对设备状态全盲(输出可能还开着) | Mutex 只配做同机快速信号,状态与交接语义必须在 DB 表(§3 方案②评估) |
| 网络共享盘(SMB)上放 data.db 供多机写 | SQLite 锁在 SMB 语义不可靠 → 双持有者 = 真实炸机风险 | 多机场景:Phase 3 Broker(每台物理机一份 DB)或租约表迁到服务端 SQL |
| 客户端自己算 expires_at 传绝对时间 | 时钟漂移导致提前/延迟回收 | 只传相对 TTL,DB 侧 datetime('now') 计算(§7) |
| TTL 设 5 分钟"求稳" | 崩溃后其他工位干等 5 分钟 | TTL 30s + 心跳 10s;同机 PID 看门狗秒级兜底 |
| 抢占发生在一条多字节 CAN 事务中间 | 设备卡在半包状态 | 让渡点严格对齐 TestStep 边界/单条命令完成(§5.4 铁律③) |
| 维护模式不做栅栏校验"反正有 UI 弹窗" | UI 弹窗可被忽略,程序 bug 绕不过 | 驱动入口 FenceCheck 是物理闸门(§7 僵尸行),弹窗只是体验层 |
| 心跳挂在 UI Dispatcher | WPF 主线程卡顿→假死误判被抢占 | 独立后台线程(§9.3 代码已合规) |
device_lease 所在库放到服务器(表结构不变,DbLeaseStore 换连接工厂即可——这是 Phase 3 前的最小台阶)。IResourceManager 的跨进程实现,上层 IAutoDeviceFacade/AutoDeviceLease/DebugSessionManager 接口签名与业务代码零改动(Phase 1,3~4 人日)。ConnectionUtil.PipeServer/PipeClient,不必引入 WCF/gRPC。access_mode 显式建模(注册页可选),工位分组复用 SlotDeviceMapping + 组租约按 key 字典序原子申请,杜绝死锁。