fix(runtime): 每个请求从自己解析出的 kernel 取服务 —— 多租户 host 上两个请求不再互相串改 (#5155) - #5385
Merged
Merged
Conversation
…patcher (#5155) One HttpDispatcher serves a whole host, but the kernel a request resolves to is per request. That answer was stored on the instance field `this.kernel`, written once per request by `resolveRequestScope()` and read by every service lookup afterwards — each behind at least one `await`. Two interleaved requests on a multi-tenant host therefore swapped data sources under each other: A resolved env-1, yielded, B resolved env-2, and A resumed reading env-2. `HttpProtocolContext` now carries `kernel`, written by `resolveRequestScope()` next to the `environmentId` / `dataDriver` / `executionContext` it already writes there. `this.kernel` is gone. Every kernel-reading member of `DomainHandlerDeps` / `ActionExecutionDeps` takes the request as its first parameter, so the dependency is visible at the call site and the compiler asks for it — chosen over AsyncLocalStorage, which would have reintroduced implicit mutable ambient context, the same defect in a new costume. Three host-level readers (`/ready`, its driver-health probe, the memoized `default-project` lookup) now name `defaultKernel` explicitly instead of reading whichever tenant resolved most recently. Covered by a deterministic interleaving regression test: request A parks inside its own identity resolution, request B runs to completion, A resumes. On the old code A is served env-2's i18n bundle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 5, 2026 03:20
baozhoutao
enabled auto-merge
August 5, 2026 03:20
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 30972041566 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5155
先证可达性,再动修法
按 PM 裁定的顺序:先写交错回归测试,在未改动的
origin/main上跑,证实「请求 A 跨 await 读到请求 B 的 kernel」确实可达。新增
packages/runtime/src/http-dispatcher.multi-tenant-concurrency.test.ts。交错是确定性的,不靠调度时序:请求 A 在自己的身份解析里被一个测试持有的闸门 park 住(它那个 kernel 的auth查找 await 住),请求 B 全程跑完,然后才放行 A。在改动前的代码上:
请求 A(env-1)拿到的是 env-2 的 i18n 包。issue 的前提成立,而且是走完整
dispatch()管线复现的,不是构造出来的。事实核对
issue 引用的行号已漂移(#5237、E 系列之后),但代码事实逐条成立:
this.kernel是实例字段,resolveRequestScope()每请求写一次,resolveService()/getService()/getObjectQL()/getRequestKernelService()/announceKernelEvent()/getRegisteredAiRoutes()全部从它读,每一个都在至少一个 await 之后。Node 单线程不保护这个 —— 它保护的是「不跨 await 持有可变共享状态」的代码。修法:方案 A(显式参数)
HttpProtocolContext新增kernel字段,由resolveRequestScope()写入 —— 和它本来就在那儿写的environmentId/dataDriver/executionContext并排。this.kernel整个删掉。DomainHandlerDeps/ActionExecutionDeps上每一个读 kernel 的成员,第一个参数改成请求本身。选它而不是 B(AsyncLocalStorage):后者把隐式可变上下文又请回来一次,正是这次事故换个壳。显式参数让依赖在调用点可见,而且编译器会替你要 —— 26 个文件、65 个调用点全是 tsc 点名出来的,没有一处靠人肉扫。为什么传的是 context 而不是裸 kernel:
resolveProjectKernelObjectQL(context)这个 seam 要把换好的 kernel 写回去(domains/actions.ts:125换完之后还继续做deps.*查找,必须看到换后的值)。传裸值的话这个写回无处可去,得靠调用方手工接返回值再逐层传 —— 那是新的一类容易写错的地方。context 本来就是每请求对象,kernel 明明白白挂在context.kernel上,调用点读得到。顺带修掉的同源问题:
/ready、它的 driver 健康探针、以及记忆化的default-project查找,这三个是副本级而不是请求级的读,原来同样读this.kernel,也就是「读到最近一个租户的 kernel」。现在显式读defaultKernel。getDiscoveryInfo(prefix)加了个可选第二参数;适配器和 dispatcher plugin 直接从 host 服务/discovery的路径不用改,现在确定性地描述 host kernel,而不是最后一个请求的租户。反向验证
方向是跑之前先定的:把共享可变字段放回去(在
requestKernel()前面插一个__revertProbeLastKernel),预测测试 1、2 变红,测试 3 保持绿 —— 因为缺陷在读不在写,context.kernel仍然写得对。实测完全一致:探针已移除。
夹具处置
5 个既有测试文件的 fake deps 是「用了旧签名」这一类,按改写处置(不是整体替换):fake 现在照新 arity 接 context。这不是走过场 —— 一个漏传 context 的调用点会让
name收到 envId,查找落空,测试就红,所以 arity 本身是有牙的。验证
按消费半径扫过跨包夹具(#5046 的教训),改动确认只落在
packages/runtime—— 仓库内与两个兄弟仓都没有DomainHandlerDeps/ActionExecutionDeps的实现方或调用方。跨包回归:门禁:
check-nul-bytesOK、check:startup-registry-verdictOK、check:durability-log-levelOK、eslint 干净。边界
matchEndpoint取数逻辑 —— 那是 声明式端点的两个机器可读面会说谎:runtime-authoredapi行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224 的裁决面(Blocked-byapi不在 metadata 类型注册表里 —— Studio 直写路径完全不校验端点,publishPackageDrafts 也没有 E7 门 #5206)。本 PR 只动每请求 kernel 状态。this.kernel === defaultKernel恒成立,赋值本来就幂等),这也正是本地和 CI 一直看不见它的原因。Generated by Claude Code