第 03 篇用 end() 保证 Span 交付。但在一段嵌套调用中结束了子 Span,并不自动使父 Span 重新成为当前 Span;反过来,关闭 Scope 也不会结束 Span。要避免把订单请求的父上下文错挂到下一次工作上,必须先分清当前上下文与 Span 生命周期。

一个线程上的嵌套栈

1
2
3
4
5
6
7
初始 Context
-> outer.makeCurrent() 当前为 outer
-> inner.makeCurrent() 当前为 inner
<- innerScope.close() 当前恢复 outer
<- outerScope.close() 当前恢复初始 Context

inner.end() / outer.end() 单独负责结束与处理器交付

公开 Span.makeCurrent() 把含该 Span 的 Context 设为当前,返回必须关闭的 Scope。Span.current() 读取当前 Context 中的 Span,但 startSpan() 不是“自动入栈”:要让后续创建的 Span 以它为父,通常先进入 makeCurrent() 的作用域,或显式 setParent(context)。因此两个并列的 startSpan() 不一定构成父子关系。

makeCurrent 究竟恢复了什么

上游 Context.makeCurrent 进入 ContextStorage.get().attach(this)。默认线程本地实现 ThreadLocalContextStorage.attach 保存进入前的 Context,并绑定新 Context;ScopeImpl.close 只在该 Scope 仍是当前作用域且尚未关闭时恢复旧 Context。关闭非当前 Scope 的分支不负责强行修改上下文。这段内部 storage 可替换,业务不应依赖具体 ThreadLocal 类名;公开契约是成对进入和退出 Scope。

注意它只恢复当前上下文,没有调用 Span.end()。Span 结束则由第 03 篇的 SdkSpan.endInternal 更新状态并触发 processor;两个状态维度独立。当前 Context 的作用范围是运行线程,不会自动跨 Executor 切换。上游 ContextTest 覆盖嵌套进入和恢复,关闭非当前 Scope 的分支可见 第 121 行。这些是静态实现与上游测试位置;本系列另在本机复核了下面的行为。

异常退出与泄漏反例

运行 ./mvnw -q -pl sdk-labs -Dtest=Lab04Test test。Lab04Test 先嵌套进入 outer / inner:在内层抛异常,由 try-with-resources 自动关闭 inner Scope;捕获处读到的是 outer,且内存导出器仍为空。然后代码明确结束 inner;退出外层时恢复最初的 Span,明确结束 outer 后导出器才有两条记录,子 Span 的 parent ID 与 outer 的 Span ID 相同。

第二个测试在专用单线程池中演示泄漏反例:一个任务进入 Scope 后故意暂不关闭,下一任务在同一工作线程读到上一个任务的 Span ID。随后在该工作线程的 finally 清理动作中关闭原 Scope、结束 Span,第三个任务验证当前 Context 已清空。它不污染测试运行线程或共享线程池,也不是生产代码可采用的模式。

1
2
LAB04 nested=restored scopeClose!=spanEnd finished=2
LAB04 isolated-leak=visible-to-next-task worker-after-cleanup=empty

“异常退出”并非自动安全:安全来自 try-with-resources 的 Scope.close(),而不是 JVM 替人清理 ThreadLocal。反例同样说明线程池复用时漏关 Scope 可能导致跨请求错链甚至数据隔离问题;若只能在别的线程上关闭 Scope,不能假定会修复原工作线程的绑定。

边界与误解

关闭 Scope 不结束 Span;结束 Span 不关闭 Scope。Span.current() 是当前线程的查询,不是遍历所有活动请求的注册表。把二者独立管理,才能让调用链父子关系与资源释放各有可验证的断言。

练习一。 从 Lab04Test 的内层删掉 inner.end(),只保留 innerScope.close();预测完成列表和恢复后的 Span.current() 各怎样变化。

练习二。 在隔离泄漏测试中,把“下一任务观察”换成创建一个新 Span;检查它的父 ID 是否误指向上一任务,最后仍须在原线程的 finally 清理 Scope。

系列导航与资料

00 导读 · 01 初始化 · 02 字段归属 · 03 生命周期 · 当前篇:04 Context 与 Scope。下一篇:05 异步传播(待写)。

参考资料:OpenTelemetry Context 规范、上文固定 SHA 的 Java 实现与测试(Apache-2.0);教学实验 Lab04Test 的命令和输出在工程 evidence/04/。