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.tsstart()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. 正确性)。

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions