Tokio 任务报 span already closed:检查跨 await 的 enter guard
tracing-subscriber 0.3.23 的关闭 span 断言可能由跨 await 的 enter guard 触发。改用 Instrument 绑定 future 轮询,检查任务结果与追踪父子关系。
更新 tracing-subscriber 后,Tokio 任务在 Span::current() 或创建 span 时 panic,报错包含:
tried to clone a span (...) that already closed
若代码里存在 let _entered = span.enter();,而这个 guard 的生命周期跨过了 .await,先修正 span 的使用范围。tracing 0.1.44 配 tracing-subscriber 0.3.22、0.3.23 有这一具体触发场景;旧版不 panic,并不能证明这项用法正确。
enter 的范围属于当前线程,不属于整个异步任务
span.enter() 返回的 guard 在释放时退出 span。同步调用中,这可以围住一段连续执行的代码;异步任务遇到 await 后却可能暂停,让别的任务在当前线程运行,也可能在另一个工作线程恢复。
guard 仍活着,不表示原任务一直占着那个线程。结果可能是某个线程仍把已经关闭的 span 当作当前 span,另一条任务随后读取或克隆它时才暴露异常。报错的 Span::current() 因此不一定就是最先误用 enter 的位置。
排查时搜索进入 span 的所有位置,检查 guard 的实际结束范围。不要只给发生 panic 的 Span::current 外面加捕获,也不要只删除这条日志;不平衡的当前 span 状态可能继续影响其他任务。
将 span 绑定到 future 的每次轮询
跨异步挂起的范围使用 Instrument。下面只演示一个任务的绑定方式,subscriber 仍按应用原来的初始化入口配置:
use tracing::{info_span, Instrument};
async fn worker() {
tokio::task::yield_now().await;
tracing::info!("worker resumed");
}
async fn run_worker() -> Result<(), tokio::task::JoinError> {
let task = tokio::spawn(
worker().instrument(info_span!("worker")),
);
task.await
}
Instrument 在 future 被轮询时进入 span,在这次轮询结束时退出,因此不会让 span 的进入状态一直占住等待期间的线程。原来跨 await 保存的 enter guard 应移除;仅给外层再套一个 instrument,而保留错误 guard,不是完整修正。
如果只需要给一小段同步准备工作设置 span,可在明确的同步作用域里进入,并在 await 之前结束。把“创建 future 的同步动作”放进作用域,不等于把 future 后续执行也纳入了这个 span。
已有父 span、任务命名和字段应按应用需要保留。独立 spawn 的任务是否继承当前上下文,也要显式检查,不能只让 panic 消失,却把所有事件归到一个错误父节点。
单线程运行时也不是修复办法
即使任务不会换线程,await 期间仍可能交错运行其他任务。把 Tokio 改成 current-thread,或暂时降低并发,可能改变复现概率,却没有使跨 await 的 enter guard 获得正确语义。
验收时保留会发生挂起和交错的任务路径,核对任务退出结果、日志中的 span 关系,以及正常、取消和错误分支。不要忽略 JoinHandle 后只看主进程仍在运行;一个后台任务已经 panic,主进程也可能继续存活。
较新的依赖可能改变断言或报错信息,但不能替应用修正 guard 范围。不要删除注册表里的断言、吞掉所有 panic 或仅回退到不报错的旧版本,再把缺少异常当作追踪上下文已经正确。


