Repository navigation
HttpDispatcher 把「本请求解析出的 kernel」存在实例字段上(this.kernel),多租户 host 上并发请求会互相串改 #5155
Description
Activity
🔒 认领: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 裁定,留否决窗口):
- 先可达性,后修法(按 issue 自己的建议):先写多租户 kernel-resolver 交错回归测试,证实「请求 A 跨 await 读到请求 B 的 kernel」确实可达,再动手修 —— 该测试同时是修复的钉子。
- 修法偏好 A(kernel 作为显式参数逐层传入域处理器):防 AI 轴 —— 显式契约让后来的写入方在类型上就写不错;B(AsyncLocalStorage)引入隐式可变上下文,恰是这次事故的形状换了个壳。若实测发现 A 的面大到不可控或需动公共契约,按 needs_decision 停下来报告,不要自作折中。
- 本单是批内重活:构建/测试全程持容器级 verify 锁,谨遵 os-dev 资源纪律。
前置事实:#5224(匹配器取数面)与本单同文件,已记 Blocked-by #5206 暂缓 —— 本单落地时不要顺手动
matchEndpoint取数逻辑,那是另一单的裁决面。同日 churn:dispatcher 近日被 #5237 及 E 系列(#5129/#5111)触及,issue 引用行号可能已漂移 —— 以
origin/main为准先核对(worktree 从 origin/main 切出后不会自己更新)。
Generated by Claude Code
✅ 验收通过 —— 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-authoredapi行在 /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
- 可达性先行,按裁定顺序执行:确定性交错测试(闸门 park 请求 A,不赌调度)在未改动 main 上复现「env-1 请求拿到 env-2 的 i18n 包」,走完整
发现于 #5129(E5b 端点链接线)的实现过程中,不是本单引入的缺陷,也不在本单范围内 —— 如实记录,交 PM 分诊。
事实
packages/runtime/src/http-dispatcher.ts:每个 host 只构造一个HttpDispatcher(dispatcher-plugin.ts的start()里new HttpDispatcher(kernel, …),所有路由共享它),但「本请求解析到哪个 kernel」被写在实例字段this.kernel上:而
resolveService()/getObjectQLService()/domainDeps.getRequestKernelService()全部从this.kernel读。域处理器在这一次赋值之后会跨多个await边界继续读它。于是在多租户 host(注册了
kernel-resolver)上,两个交错的请求 A(env-1)、B(env-2)可以是:this.kernel = kernelOfEnv1;await(会话解析、driver 查询)让出;this.kernel = kernelOfEnv2;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的每一个消费者,那是一单独立改动,不该混进一次接线单。可能的修法(留给分诊,不预设结论)
DomainHandlerDeps契约,面大但显式);AsyncLocalStorage承载 per-request kernel(改动小,但引入隐式上下文);先确认可达性(写一个多租户交错的回归测试)再选,比先选修法更省。