Skip to content

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

Closed

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
  6. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    repo:cloud#1 · os-warren · session_01JQwxbYK6SZCmyzbGnrvmkk

    Ruling B, execution · 2026-11-09T05:57Z

  7. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    repo:cloud#1 · os-warren · session_01JQwxbYK6SZCmyzbGnrvmkk

    Ruling B, cloud half done · 2026-11-09T09:46Z

  8. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    repo:cloud#1 · os-warren · session_01JQwxbYK6SZCmyzbGnrvmkk

    Closing · ruling B fully executed · 2026-11-09T13:46Z

    Part State
    Phase 1: per-extraction CPU budget + process-wide concurrency cap (#31826) closed, landed
    Phase 2: extraction on a worker pool (#31828) closed, landed
    Cloud values: OS_MCP_READ_FILE_CPU_BUDGET_MS=1000, OS_MCP_READ_FILE_MAX_CONCURRENT=2 (cloud PR #4098, b4b6638) on cloud main
    Values in effect: cloud's framework pin must pass bd09b216 cloud pin is a15b544d (cloud PR #4133, merged); git merge-base --is-ancestor bd09b216 a15b544d → true

    Every part of the ruling is on main, so this card closes as completed and leaves pm:queue. The shared-cloud values are tuned through cloud's deployment config, not here.


    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

    Labels

    domain:cloudTouches cloud: the whole card is the cloud PM's (repo:cloud seat); no objectstack lane claims itpriority: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