Skip to content

[Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100

Open

Description

@objectstack-fleet

domain:server#2 · os-tesla · session_01L9qibzuTqmB5hqFYmMATiY

立卡门:② 只有维护者能做的决定 + ③ 维护者直派。维护者原话(2026-11-07,项目线程):「立一张 cloud 车道的决策卡」。前一问原话:「read_file 这个服务端转换需要消耗很多算力吗?比如我们是共享云」。

维护者速读

背景

read_file(packages/mcp/src/file-content.ts,PR #31009 head da6a24cb)的上限今天全是单次上限:文件超过 10 MiB 不读;PDF 最多 200 页或 10 万字;Word 解压后最多 32 MiB;图片原样转交、不转换。没有并发上限、没有时间预算、没有按租户限流。

实测(本机 4 核,unpdf 1.9.1,与 extractPdfText 同参数,每例 3 次取稳定值):

文件 大小 读到 CPU 期间事件循环被占住
200 页、每页少量文字 0.14 MiB 200 页 / 8.7 万字 约 80 ms 全程(约 75 ms)
200 页、大量绘图指令 + 少量文字 9.25 MiB 200 页 / 2.9 万字 约 320 ms 全程(约 300 ms)
200 页、密排文字 0.64 MiB 35 页即到 10 万字上限 约 65 ms 全程(约 50 ms)

「全程」的读法:抽取期间一个 1 ms 的定时器一次都没能触发,即同一进程的其他请求在这段时间里全部排队。单次不重,但它是串行独占的:同一进程同时来 10 个大 PDF,就是 3 秒左右谁都动不了。

a security item is with the maintainer.

Governing text

  • docs/adr/0102-sandbox-cpu-budget-and-engine-variant.md D1:租户提供的脚本按每次调用的 CPU 时间预算计费,墙钟只作单独的上限。这是平台对「租户提供的输入在共享进程里跑」的既有口径。
  • 检索 grep -rlniE "event loop|worker_threads|concurrency (cap|limit)|noisy neighbo" docs/adr content/docs → 只命中 ADR-0102;文件抽取没有专门条文。

协议声明:Metadata-protocol: none。四个选项都不改 spec、不改 /meta。

前提(带复核命令)

  1. 抽取没有并发上限和时间预算:grep -cE "concurren|semaphore|timeout|AbortSignal|deadline" packages/mcp/src/file-content.ts → 0(head da6a24cb)。
  2. 抽取在请求线程上同步占住事件循环:上表最后一列。复测:照 extractPdfText 的参数调 unpdf.getDocumentProxy + 逐页 getTextContent,同时挂一个 1 ms setInterval 计触发次数。
  3. 仓内没有 worker 线程池先例:grep -rln "worker_threads\|new Worker(" packages --include=*.ts | grep -v test → 1 个无关文件。

具体问题

共享云上,一个租户的助手读文件,会不会、以及允许多大程度上,拖慢别的租户?

选项

选项 做什么 客户能感到的后果 工作量
A 平台加两道闸:每次抽取的 CPU 时间预算(到时停,返回已读部分 + "其余请下载")和全进程并发上限(满了答"稍后再试")。取值由宿主环境变量配置 普通文件无感;极端文件只拿到部分文字;高峰时偶尔要重试。别的租户不再被长时间卡住,但每次抽取仍会占住进程几百毫秒 server · 1 PR(可并入 #31009)· 无同形先例
B A 的两道闸 + 第二期把抽取移到独立 worker 线程池,请求线程不再被占 同 A,且读文件完全不拖慢同进程的其他请求 server · 2 PR(第一期同 A,第二期 worker 池)· 无先例
C 平台不动,cloud 控制面按租户限 read_file 次数或计量 限住"量",限不住"单次多重";同一进程里的别人仍会被卡 cloud · 1 PR · 无先例
D 维持现状,上线后看监控再定 出事之前无感,出事时是全进程变慢,客户看到的是"系统卡" 无

业务含义直译:

  • A:给每台复印机装一个"单次最多印多久"的计时器,加一个排队号。
  • B:A 加上把复印机搬出前台,排队的人不再挡住柜台。
  • C:每家公司每天限印几次,但一家印一本厚书时柜台照样被堵。
  • D:先不管,等有人投诉再说。

四棱

  • ① 项目长远合理性:两年后该能力的样子是:文件抽取跑在请求线程之外、每次有 CPU 预算、取值由宿主配置,和 ADR-0102 的沙箱同一口径。主流平台(Salesforce 的 Apex governor limits、Cloudflare Workers 的每请求 CPU 上限)都是"平台定机制、按次计 CPU"。B 收敛到这一个口径;A 是 B 的一半;C 把平台该有的机制挪到应用仓,扩大特例;D 不收敛。
  • ② 实际业务拉动:今天没人撞上(read_file 未上线)。但共享云是默认部署形态,上线即每个开了 MCP 的租户都能触发,拉动来自上线本身。
  • ③ 防 AI 犯错:AI 助手会照用户的话连读多份文件,不会自己节流。A/B 出错时是响亮的("稍后再试"或"只读了一部分,请下载");D 出错时是静默的(别的租户变慢,没人知道原因)。
  • ④ 创业阶段不扩散:A/B 各新增 1–2 个宿主环境变量,是永久义务;C 新增一套 cloud 配额模型,义务更大;D 零新增。
    Prior rulings read: read_file PDF / pdf extraction concurrency / MCP rate limit tenant / domain:cloud+needs-user-decision → 1 / 8 / 117 / 0 hits; ADR-0102 D1; thread: 项目线程(本卡来源)

推荐

B,分两期:第一期(时间预算 + 并发上限)写进 #31009,合并前落地,这样 read_file 从上线第一天就有闸;第二期 worker 线程池另立 server 卡,在第一期之后。

  • 不取 A:它解决"卡多久",不解决"卡住"本身;作为 B 的第一期已全部包含。
  • 不取 C:应用仓不自建平台该有的机制;按租户计量可以以后在 B 之上加,不替代它。
  • 不取 D:出事时的症状是全进程变慢,排查成本远高于现在加两道闸。

置信缺口:第二期 worker 池的工作量无先例,我的估计是 1 个 PR;实测只在本机 4 核做了,生产机型的数字会不同,但"全程占住事件循环"这一点与机型无关。

自检行:只看①选 B;②③④ 是否翻转:否(②只影响时序,已体现在分期;④的新增环境变量 A 与 B 相同)。

Analysis-tier: seat (Opus), not the ceiling tier.

裁后执行

相关

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)
    domain:cloudTouches cloud: the whole card is the cloud PM's (repo:cloud seat); no objectstack lane claims it
    priority:p1High: required for production / M2
    on Nov 7, 2026
  2. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: grade · p1 · domain:cloud · 2026-11-07T07:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH

    Status · 2026-11-07T08:28Z · PR #31009 merged 2026-11-07T08:08Z as 704efef8; the maintainer enqueued it before this card was ruled. So read_file's PDF extraction is on main without a CPU budget or a concurrency cap. Option B's phase 1 can no longer ride #31009. If B (or any bound) is chosen, it needs its own domain:server PR against main. The server seat 2 lane is signing off, so that PR goes to the online server seat (seat 1, mailbox #31077).

  4. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Ruling: batch #T962 item 2 · letter B · maintainer 「同意」 2026-11-07T14:47Z

    Path: none

    Presented in the triage seat's session as batch 1 item 2. The maintainer answered 「同意」 to B, in two phases. PR #31009 had already merged (08:08Z), so phase 1 now follows it instead of landing inside it.

    What binds:

    • Phase 1: a per-extraction CPU-time budget (a partial text plus "download the rest" when it runs out) and a process-wide concurrency cap ("try again shortly"). Both values are configured by the host.
    • Phase 2: extraction moves off the request thread, onto a worker pool.

    Execution:

    • domain:server files phase 1 as its own card now. Its defaults are documented in content/docs/ai/connect-mcp.mdx, with ablation tests.
    • The cloud PM seat sets the shared cloud's values in its deployment config.
    • Phase 2 is a second server card, Blocked-by: phase 1.
    • This card closes once both cards exist and the cloud value is set.

    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:cloudTouches cloud: the whole card is the cloud PM's (repo:cloud seat); no objectstack lane claims itpm:queueReady for the PM dispatch looppriority:p1High: required for production / M2repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions