LabHub
学习 学习路径 课程

OTCA — OpenTelemetry 认证助理

同一个函数,却带上另一订单的标签

在 LabHub 中继续学习

一句话总结

上下文问题不仅关乎值是否存在,还关乎何时复制、在哪里恢复。不要把 Task、延迟 coroutine 和普通线程池当作同一种执行边界;通过几行观测,就能缩小请求串线的原因范围。

概念图: 一句话总结 · 为什么需要它 · 工作原理 · 创建值与附加为当前值

为什么需要它

某服务同时处理两笔订单。收到订单 A 时设置 request=order-a 并调度库存检查,随后调用方的值改成另一请求;库存日志却打印了后一个请求标签而非 A。函数参数正常、进程也只有一个,起初很像日志服务器排序错误。

本实验无需外部服务器即可重现。它在安装 Python 3.12 与 OpenTelemetry SDK 1.44.0 的环境中运行真实 asyncio 和 ThreadPoolExecutor,不是向假日志写入答案句子。它分别读取回调执行瞬间的 baggage,以及函数返回后调用方的 baggage 进行比较。

前一模块问“span 是否收到”,这里问“这项工作以谁的上下文运行”。即使大量观测数据到达,把不同请求错误连接,根因分析仍会出错。因此增加采集量前应先确认连接语义。

工作原理

创建值与附加为当前值

OpenTelemetry 的 baggage.set_baggage 返回一个装有值的 Context,不等于用 attach 把值应用到当前执行。使用 attach 返回的 token 调用 detach,才能恢复进入前的上下文。请区分 Python Context APIBaggage API 的契约。仅仅创建 Context,并不表示回调就会读到它。

第一步是一个小作用域:让回调看到内部值,同时保存原调用方。调用方值为 caller,内部值为 order-a。分别执行正常返回和抛出 ValueError 的回调。即使内部值正确,外部仍停留在 order-a,也只对了一半。丢弃返回值或把异常替换成新异常,也会破坏业务代码契约。

若只在成功路径末尾恢复,中途异常会跳过恢复行。因此,正常与异常路径都必须观测到原值。只运行一次请求就退出进程的测试很难发现该缺陷,还需要检查值残留后会运行什么。

写下三个时点

预实验先附加 alpha,再创建工作;随后把调用方改为 beta,再等待工作。执行顺序看似相同,worker 读到的值却取决于创建工作的 API。

在 alpha 时做的事 改为 beta 后等待的实测结果
create_task(coro) Task 读取 alpha
只创建 to_thread(function) coroutine 线程读取 beta
create_task(to_thread(function)) 线程读取 alpha

这是本模块在 Python 3.12 的实验结果。三者都是“以后执行的工作”,捕获 Context 的时点却不同。第二行只是创建 coroutine 对象,尚未运行其正文;第三行则把该 coroutine 作为当前上下文的 Task 调度,与之后的调用方变化隔离。

Python 文档说明 create_task 默认使用当前 Context 的副本,单纯调用 coroutine 并不会调度执行,并把取消清理与 try/finally 联系起来。Python 3.12 asyncio。将该契约与实测值对照,比“async 会自动传播”或“线程一定丢失”更准确。

实验中的 start_task 与 start_thread 要保存调度时的请求。为了让 worker 读到正确值,而永久改回调用方当前值,并不是答案;调用方应继续保持 later-caller。只有同时观察两边,才能区分传播与泄漏。返回已调度 Task,使调用方能等待和取消它;不要把这推广为丢弃结果和引用的后台任务模式。

普通 executor 是独立实验

普通 ThreadPoolExecutor.submit 在预实验中不会自动传递 alpha。在提交方用 contextvars.copy_context 复制,再在副本的 run 中执行函数后,才观测到 alpha。随后向同一 worker 提交普通函数时则没有值,分别确认了“曾经传递”和“没有残留到下一任务”。

若在 worker 函数内部复制,跨过边界后才复制到错误上下文。代码中即使有 copy_context,执行顺序错误仍会失败。评分器比较真实返回值,而非搜索单词。每次提交建立独立副本,并保护复用 worker 的状态,是本阶段任务。每次新建线程会掩盖复用泄漏,所以会用同一 worker 检查两笔订单。

取消也是请求的结束

客户断开连接或上层任务取消时,回调可能无法正常返回。第 5 步用 Event 把两个请求汇聚到同一点后继续执行,检查重叠区间的值,再触发异常与取消并确认恢复。最后对真实等待中的 Task 调用 cancel,避免只验证主动抛出 CancelledError 就声称覆盖外部取消路径。

捕获取消后若像普通成功一样返回,即使内部值已恢复,调用方也不知道任务被取消。本实验契约是恢复值,同时把异常和取消继续传给调用方。清理不等于隐藏错误。

现场会遇到的情况

调查顺序如下:先把请求分成两个,使用不同合成标识;记录调用前、调度后、回调内、返回后的值;正常路径正确后再加入异常和取消;存在池时复用同一 worker;结果中记录运行环境和 API 名,避免后人用另一运行时行为误解。

本测试无需真实个人信息或生产令牌。order-a 与 order-b 足以区分传播时点和泄漏。仅需不同值时使用真实客户标识,反而会让调查资料本身成为新的管理对象。

下一实验要做什么

第 1~5 步依次修复小作用域恢复、Task 调度、线程调度、executor 提交和重叠请求的取消清理。查看每次执行的 observations 与 checks,解释哪个边界错误。下一篇理论会把“值正确传递”和“值值得信任”分开:传递成功不等于认证成功。