症状
级联有清晰的时间线。首先是一次普通 panic:临界区内 index out of bounds;然后从那以后,该 mutex 的每个使用者都失败:Err(PoisonError)。毒化是设计在正常工作:panic 在守卫存活期间展开,受保护结构可能处于改到一半的状态——后来的加锁者收到 Err 而不是操作可能已损坏的数据。诊断要看时间线而不是最后一个错误:根因是第一个 panic,其余都是遏制层。一个代码路径的 bug 升级成受保护资源的服务级宕机,就是这条链的样子。
诊断思路
先找原始 panic(时间线上第一个),其回溯显示被展开的临界区;再审计所有 lock().unwrap() ——每个 unwrap 都把毒化转成新的 panic,把一次失败放大成多次,这些行是级联面;最后决定恢复策略并写下来,而不是默认处理。
根因
std::sync::Mutex 在守卫存活期间发生 panic 展开时毒化自己,因为受保护数据可能处于变更中途、不变量已破坏。之后每次 lock() 都返回 Err(PoisonError) 以防止操作可能损坏的状态——这个安全特性把一次 panic 变成持久的资源故障。
修复与验证
先修原始 panic,否则一切都不持久。恢复策略二选一:不变量可证明是原子的(全有或全无更新)用 into_inner() 直接恢复;可修复的用显式校验加重建(valid() 检查、rebuild() 修复并文档化)。长期预防:临界区保持小且无 panic、校验逻辑放锁外、按语义考虑 parking_lot::Mutex 或 tokio::sync::Mutex(都不毒化)。验证时把时间线写进测试:故意在临界区 panic、catch_unwind 捕获、跑恢复路径、断言受保护数据一致且后续加锁者拿到 Ok;再在修掉原始 panic 后重跑,断言毒化完全不再发生。两个断言合起来证明遏制有效且根因已除。
代码示例
thread 'worker' panicked at 'index out of bounds', src/inventory.rs:88
Err(PoisonError<...>) at src/inventory.rs:31
// invariant provably atomic (all-or-nothing update): recover directly
let mut state = match lock.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(), // data intact by construction
};
// invariant repairable: validate then fix
let mut state = match lock.lock() {
Ok(g) => g,
Err(poisoned) => {
let mut g = poisoned.into_inner();
if !g.valid() { g.rebuild(); } // explicit repair, documented
g
}
};