Skip to content

fix(runtime): 每个请求从自己解析出的 kernel 取服务 —— 多租户 host 上两个请求不再互相串改 (#5155) - #5385

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5155-dispatcher-request-kernel
Aug 5, 2026
Merged

fix(runtime): 每个请求从自己解析出的 kernel 取服务 —— 多租户 host 上两个请求不再互相串改 (#5155)#5385
baozhoutao merged 1 commit into
mainfrom
claude/issue-5155-dispatcher-request-kernel

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

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。

在改动前的代码上:

AssertionError: expected [ 'env-2-locale' ] to deeply equal [ 'env-1-locale' ]
- Expected     + Received
-   "env-1-locale",
+   "env-2-locale",

请求 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 仍然写得对。实测完全一致:

× an interleaved request does not move the first request onto the other tenant's kernel
× the endpoint fallback path resolves services on the scope it resolved, not the latest one
AssertionError: expected [ 'env-2-locale' ] to deeply equal [ 'env-1-locale' ]
AssertionError: expected 'env-2' to be 'env-1'
      Tests  2 failed | 1 passed (3)

探针已移除。

夹具处置

5 个既有测试文件的 fake deps 是「用了旧签名」这一类,按改写处置(不是整体替换):fake 现在照新 arity 接 context。这不是走过场 —— 一个漏传 context 的调用点会让 name 收到 envId,查找落空,测试就红,所以 arity 本身是有牙的。

验证

pnpm --filter @objectstack/runtime typecheck   → Done(0 error)
pnpm --filter @objectstack/runtime test        → Test Files 91 passed (91) / Tests 1336 passed (1336)

按消费半径扫过跨包夹具(#5046 的教训),改动确认只落在 packages/runtime —— 仓库内与两个兄弟仓都没有 DomainHandlerDeps / ActionExecutionDeps 的实现方或调用方。跨包回归:

@objectstack/http-conformance  46 passed   (跨适配器一致性套件)
@objectstack/hono              73 passed
@objectstack/client           222 passed
@objectstack/cloud-connection  89 passed
@objectstack/verify            17 passed

门禁:check-nul-bytes OK、check:startup-registry-verdict OK、check:durability-log-level OK、eslint 干净。

边界


Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 3:16am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 30972041566 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (3/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/commands/serve-tenancy-posture-gate.test.ts�[2m > �[22mthe gate runs before serve does ANY boot work�[2m > �[22mrefuses before the config file is even read, and writes no
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 1 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants