Coroutines 1.11.0 的 Flow 转 Flux 报 observation 不匹配,检查上下文恢复顺序
Flow 转 Flux 的 undispatched 挂起路径可能打乱 ThreadContextElement 恢复顺序,导致 observation 断言或 trace 串链;核对整组依赖和 dispatcher,按完整路径回归。
WebFlux 请求中的 Flow 转成 Flux 后,日志出现:
AssertionError: Expected current observation to be ... but was ...
CoroutinesInternalError
接口甚至可能继续返回,构建任务也显示成功。若问题出现在 kotlinx.coroutines 1.11.0,并经过带有线程上下文元素的 Flow/Flux 适配路径,要检查挂起前后的上下文恢复,而不是先把它当成业务层重复关闭 span。
明确的故障组合包括 Spring Framework 7.0.8、Reactor 3.8.6 与 coroutines 1.11.0。独立的 ThreadContextElement 跟踪也能观察到作用域关闭顺序不一致,因此不应把它限定为某一个 Micrometer 指标名导致的异常。
调用次数相等,顺序也可能错
线程上下文通常先保存旧值、安装新值,退出时再恢复。对于维护栈式 scope 的实现,嵌套安装必须按相反顺序撤回。
1.11.0 的 withContextUndispatched 调整了 withCoroutineContext 与挂起原语的嵌套。内部挂起时,部分恢复工作延后到继续执行,外层的清理却先发生,于是线程上还安装着内层 scope,外层就试图恢复自己的状态。
因此不能只数 updateThreadContext 与 restoreThreadContext 各调用几次。已核证的边界是顺序与生命周期不对;声称“restore 永远少一次”会把诊断带偏。
观察实际 dispatcher、flowOn、asFlux 的 context 和 Reactor subscriber context。上游确认的 undispatched 分支与其他显式 dispatcher 不同;没有这些上下文条件时,不能仅凭一个 observation 断言就确定是这条回归。
临时处理要保留追踪与执行语义
对于已经确认受影响、仍能兼容旧版的工程,可以评估将整组 coroutines 依赖固定回 1.10.2,作为回到已知工作组合的临时办法。先检查 Spring、Kotlin 插件及所用 API 的兼容要求,不能只降 kotlinx-coroutines-core,留下 reactor、reactive、test 模块仍在另一版本。
Gradle 项目可先查看实际解析版本:
./gradlew dependencyInsight --dependency kotlinx-coroutines-core --configuration runtimeClasspath
./gradlew dependencyInsight --dependency kotlinx-coroutines-reactor --configuration runtimeClasspath
多模块项目在实际服务模块运行对应任务;Android 或自定义 configuration 应选择自己的真实运行变体,不能假定都有这两个顶层配置。
另一条诊断路径是评估能否为相关上游 flow 明确使用适当 dispatcher,避开已确认的 undispatched 条件。但改变 dispatcher 会改变执行位置,存在事务、线程亲和或阻塞调用时,需要按业务重新审查,不能给所有 Flow 统一加 Dispatchers.Default。
不要删除 tracing、吞掉断言,或手改 coroutines 内部函数来让日志安静。简单倒回源码嵌套顺序还可能重新引入此前修过的 flowOn 上下文错误,并不是完整修复。
验证挂起点与追踪父子关系
在受控环境保留原 SharedFlow、filter/map、非空 subscriber context 和真正的挂起点,跟踪 scope 安装与撤回的顺序。验证每个请求的 observation/span 父子关系、请求结束后线程上下文是否恢复,以及连续请求之间有没有串链。
仅有 BUILD SUCCESSFUL、HTTP 200 或没有进程退出都不够;未被正常传播的协程异常可能只留在输出中。还要查看 CoroutinesInternalError、scope assertion 与实际 trace。
截至 2026 年 10 月 21 日,对应上游主问题仍开放,重复问题关闭不代表已经发布修复。采用后续版本前核对正式记录,撤回临时版本或 dispatcher 调整时,再覆盖原来的挂起路径。


