Skip to content

ADR-0049:spec 两份 RateLimitConfig 全仓零 reader,真正在限流的是 runtime 自己的第三份形状 #4686

Closed
2 of 2 issues completed

Description

@os-zhuang

在 #4684(双源清账 C9)的调研中发现,记录备查,未认领。

事实

packages/spec 声明了两份 RateLimitConfig,两份都没有任何运行时读者:

$ grep -rn "RateLimitConfig" --include=*.ts packages/ core/ examples/ apps/ \
    | grep -v node_modules | grep -v /dist/ | grep -v "packages/spec/"
(无输出)

对具体键做同样的扫描(windowMs / windowSeconds / maxRequests / burstCapacity / respectUpstreamLimits),packages/spec/src/** 自己的测试之外同样零命中。

四个嵌入点因此全部是「声明了但没人执行」:

嵌入点 声明来源
ApiEndpointSchema.rateLimit(api/endpoint.zod.ts:47) shared/http.zod.ts
ApiEndpointRegistrationSchema.rateLimit(api/registry.zod.ts:379) shared/http.zod.ts
HttpServerConfigSchema.security.rateLimit(system/http-server.zod.ts:71) shared/http.zod.ts
ConnectorSchema.rateLimitConfig(integration/connector.zod.ts:674) integration/connector.zod.ts

前三个从 stack.zod.ts 的 apis: 可达,第四个从 connectors: 可达 —— 都是真实的作者面,作者写下去解析通过,然后什么也不发生。

更值得注意的:真正干活的是第三份形状

packages/runtime/src/security/rate-limit.ts 有一个手写的 in-memory token bucket,它不 import spec,自带一套接口:

export interface RateLimitBucketConfig {
  capacity: number;       // 对应不上 maxRequests
  refillPerSec: number;   // 对应不上 windowMs / windowSeconds
  defaultCost?: number;   // 两侧 spec 都没有
}

所以现状是 三份 rate-limit 形状、零条从作者面到执行面的通路:两份在 spec 里给作者写,一份在 runtime 里真正限流,互不相识。这正是 Prime Directive #10 的「declared ≠ enforced」与 ADR-0049 的 enforce-or-remove 场景。

建议(留给维护者裁决,不要顺手做)

三选一,都是协议级决定:

  1. 接上执行 —— 让 dispatcher 从 ApiEndpoint.rateLimit / HttpServerConfig.security.rateLimit 推导出 RateLimitBucketConfig(capacity = maxRequests,refillPerSec = maxRequests / (windowMs / 1000)),connector 侧同理接到出站调用上。
  2. 按 ADR-0049 摘掉 —— 走完整退休套件(tombstone + D2/D3 + changeset)。
  3. 先只修 connector 侧,API 侧接执行 —— 出站/入站两个方向的执行者本来就不是同一个。

⚠️ 与 #4684 / #4535 的关系:C9 簇正在裁决这两份声明的命名/收敛路线。本单是执行面的问题,与命名路线正交,但两者的答案会互相影响 —— 如果结论是 2(摘掉一侧),C9 的双源就自动消失了。建议一起排期看。

关联:#4684、#4535、ADR-0049、Prime Directive #10

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    分诊 + 维护者裁决落档(2026-08-03,PM 会话 session_01Ehu85kbvMcrNTUJjwxvLJ9):

    裁决:拆向 —— 入站接执行,出站摘除(经交互确认,三选一里的方案 3 变体)。

    分诊时实测更新(立单于 8-02 17:10,C9 #4684 于 19:17 关闭,前提已漂移):

    结构:本单转为协调节点(⛔ 不派发),两个子单各自入队、相互独立、文件面不相交:

    最后一个子单关闭时由 PM 总结关本单。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    事实更正:立单说的「真正干活的第三份形状」其实也没在干活

    在子单 #4910 开工核查时实测(origin/main @ 2e284b2546)。本单原文写:

    packages/runtime/src/security/rate-limit.ts 有一个手写的 in-memory token bucket,它不 import spec,自带一套接口

    「不 import spec」属实,但「真正干活」不成立 —— 它零调用点,全仓入站路径没有任何 429 短路:

    $ grep -rn "new RateLimiter(" --include=*.ts --include=*.mjs --include=*.js . | grep -v node_modules | grep -v /dist/
    packages/runtime/src/security/rate-limit.test.ts:23,39,54,65,77,86,97,98   ← 全部是它自己的单测
    (无其它命中)
    
    # 阳性对照,证明扫描器有效:
    $ grep -rn "new DomainHandlerRegistry(" --include=*.ts . | grep -v node_modules
    packages/runtime/src/http-dispatcher.ts:167   ← 真实调用点找得到
    

    误判来源不是粗心:rate-limit.ts 的注释用现在时描述了一条不存在的执行链(「the dispatcher calls consume(key) … short-circuits with 429」、「tune via DispatcherPluginConfig.rateLimit」—— 该字段在 dispatcher-plugin.ts:23 里不存在)。对着这段注释读代码,任何人都会得出「执行器已就位」的结论。已单独立单 #4937。

    所以本单的准确表述应是:三份 rate-limit 形状、零条通路、且三份都没有执行者 —— 不是「两份声明 + 一份在干活」。这直接改变了 2026-08-03 裁决里「入站接执行」的工作形状:不是把声明接到已有执行器,而是从零建执行点,而执行点的 seam / key 形状 / 存储 / 配置入口全是新的公共契约决定。四问已在 #4910 展开待裁。

    顺带,入站三个嵌入点逐个复核后没有一个有可接的两端,各自立单:

    出站半边(#4911)不受本更正影响。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    🏁 父单总结关闭 —— 拆向裁决两半均已落地(2026-08-03)

    半边 子单 PR 结局
    出站(connector) #4911 #4947 ✅ MERGED ConnectorRateLimitConfig 词表按 ADR-0049 完整退役(墓碑 + D2/D3 + 孤儿枚举随删)
    入站(API/server) #4910 #5006 ✅ MERGED 从零建 seam:新 server: 窄键(security.rateLimit + trustProxy,strict)→ dispatcher 全局中间件 → 429 + Retry-After;principal 优先 / trustProxy 门控 / kernel-cache 计数

    立单命题的最终答案:「三份 rate-limit 形状、零条通路」——实测更正为「三份形状、零执行者」(runtime bucket 也无调用点,#4937)后,处置为:出站词表删除、入站声明经窄形状变为 declared = enforced、runtime bucket 获得真实调用方。三份归一,一条活通路。

    沿途修真的额外 declared≠enforced:#4937(注释成真)、IHttpServer.use() no-op(现为真 seam)。

    遗留(各有单据,不留在本单):endpoint 级两键 → #4936(needs-user-decision);七个死键 → #4938(即将请裁);composeStacks 丢键 → #5005(needs-user-decision);声明式端点整面 → #4936/#4939。


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions