Skip to content

HttpDispatcher 把「本请求解析出的 kernel」存在实例字段上(this.kernel),多租户 host 上并发请求会互相串改 #5155

Description

@os-zhuang

发现于 #5129(E5b 端点链接线)的实现过程中,不是本单引入的缺陷,也不在本单范围内 —— 如实记录,交 PM 分诊。

事实

packages/runtime/src/http-dispatcher.ts:每个 host 只构造一个 HttpDispatcher(dispatcher-plugin.ts 的 start() 里 new HttpDispatcher(kernel, …),所有路由共享它),但「本请求解析到哪个 kernel」被写在实例字段 this.kernel 上:

this.kernel = (await this.kernelResolver.resolveKernel(context, this.defaultKernel)) ?? this.defaultKernel;

而 resolveService() / getObjectQLService() / domainDeps.getRequestKernelService() 全部从 this.kernel 读。域处理器在这一次赋值之后会跨多个 await 边界继续读它。

于是在多租户 host(注册了 kernel-resolver)上,两个交错的请求 A(env-1)、B(env-2)可以是:

  1. A 解析 → this.kernel = kernelOfEnv1;
  2. A 在某个 await(会话解析、driver 查询)让出;
  3. B 解析 → this.kernel = kernelOfEnv2;
  4. A 恢复,resolveService('objectql') 读到的是 env-2 的 kernel。

Node 单线程不保护这个字段 —— 保护的前提是「不跨 await 持有可变共享状态」,而这里恰好跨了。

为什么今天不一定看得见

单 kernel 部署里 this.kernel === this.defaultKernel 恒成立,赋值是幂等的,所以本地与 CI 都不会暴露。只有注册了 kernel-resolver 的多租户分发(cloud)才有第二个取值,而那条路径没有并发交错的回归测试。

我没有复现,只有代码事实。严重度请分诊时判断:如果可达,它的形状是「一个租户的请求读到另一个租户的数据源」,不是性能问题。

与 #5129 的关系

E5b 把声明式端点步接到了同一个解析上(HttpDispatcher.resolveRequestScope,由 dispatch() 里原地抽出,行为逐字节不变),所以这个字段多了一个写入方。#5129 没有加重也没有减轻这个类:同样的赋值,同样的读法。之所以在这里记而不是在那条 PR 里顺手改,是因为修法(把 per-request 状态从实例字段挪到显式参数 / AsyncLocalStorage)会穿过 domainDeps 的每一个消费者,那是一单独立改动,不该混进一次接线单。

可能的修法(留给分诊,不预设结论)

  • A:把 kernel 作为参数逐层传进域处理器(改 DomainHandlerDeps 契约,面大但显式);
  • B:AsyncLocalStorage 承载 per-request kernel(改动小,但引入隐式上下文);
  • C:每请求 new 一个轻量 dispatcher facade(分配成本 vs. 正确性)。

先确认可达性(写一个多租户交错的回归测试)再选,比先选修法更省。

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    🔒 认领:PM 循环第 1 轮(cli 车道,v17 优先,D/最高严重度)
    会话:session_01VkPSGsX9o17MsGv3Lbxu2w
    分支:claude/issue-5155-dispatcher-request-kernel
    Worktree:objectstack-issue-5155
    域:domain:cli
    文件面:packages/runtime/src/http-dispatcher.ts、dispatcher-plugin.ts、packages/runtime/src/domains/**(DomainHandlerDeps 全部消费方)+ 多租户交错回归测试 + changeset。与同批 #5000 / #5359(均在 packages/cli)不相交;全局在飞 8 单声明面均不触 runtime dispatcher。

    执行口径(PM 裁定,留否决窗口):

    1. 先可达性,后修法(按 issue 自己的建议):先写多租户 kernel-resolver 交错回归测试,证实「请求 A 跨 await 读到请求 B 的 kernel」确实可达,再动手修 —— 该测试同时是修复的钉子。
    2. 修法偏好 A(kernel 作为显式参数逐层传入域处理器):防 AI 轴 —— 显式契约让后来的写入方在类型上就写不错;B(AsyncLocalStorage)引入隐式可变上下文,恰是这次事故的形状换了个壳。若实测发现 A 的面大到不可控或需动公共契约,按 needs_decision 停下来报告,不要自作折中。
    3. 本单是批内重活:构建/测试全程持容器级 verify 锁,谨遵 os-dev 资源纪律。

    前置事实:#5224(匹配器取数面)与本单同文件,已记 Blocked-by #5206 暂缓 —— 本单落地时不要顺手动 matchEndpoint 取数逻辑,那是另一单的裁决面。

    同日 churn:dispatcher 近日被 #5237 及 E 系列(#5129/#5111)触及,issue 引用行号可能已漂移 —— 以 origin/main 为准先核对(worktree 从 origin/main 切出后不会自己更新)。


    Generated by Claude Code

  2. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    ✅ 验收通过 —— PR #5385,转 ready 挂合并队列(队列在全绿后才会收)

    对 GitHub 实况复核(cli 车道 PM,session_01VkPSGsX9o17MsGv3Lbxu2w):

    • 可达性先行,按裁定顺序执行:确定性交错测试(闸门 park 请求 A,不赌调度)在未改动 main 上复现「env-1 请求拿到 env-2 的 i18n 包」,走完整 dispatch() 管线 —— 前提坐实,非构造。
    • 修法 = 方案 A:this.kernel 实例字段删除,kernel 落 HttpProtocolContext.kernel;DomainHandlerDeps / ActionExecutionDeps 每个读 kernel 的成员显式收请求参数,65 个调用点全部由 tsc 点名。传 context 而非裸值的理由(resolveProjectKernelObjectQL 的写回 seam)成立。
    • 范围:26 文件全在 packages/runtime + changeset(minor,带 FROM→TO 迁移表),零 releases/ 触碰。dev 申报的两份契约声明文件(domain-handler-registry.ts、action-execution.ts)属裁定原文「改契约」的必然触碰,PM 认可该 scope 解释。
    • 额外价值:三处副本级同源读(/ready、driver 健康探针、default-project 记忆化)一并改为显式 defaultKernel;守住了 声明式端点的两个机器可读面会说谎:runtime-authored api 行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224 裁决面(matchEndpoint 未碰)。
    • 测试:runtime 91 文件 / 1336 用例全绿;反向核验方向预声明且精确命中(缺陷在读不在写);五个跨包套件回归绿;夹具按「改写而非替换」处置且 arity 有牙。远端 CI 当前零红(部分仍在跑,auto-merge 等全绿)。
    • check:authorable-surface 在 --check 模式下也会重写 authorable-surface.base.json —— 一次纯核验会改工作区,且任何无关 PR 都能因此静默推进删除门的锚点 #5358 的活体标本评论已核:普通依赖构建即可静默改写 authorable-surface.base.json(baseRev 前移 + 删 110 键),触发面从 --check 扩到任意构建 —— 支撑该单已排 spec-tooling 队首的判级;dev 查重后评论而非重复立单,处置正确。

    后续:本 PR 合并后,pm:blocked 的 #5242(domains/packages.ts)按解锁条件重核前提再派。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions