Skip to content

fix: release request input buffers before long-running responses #4753

Description

@z0d0dOr

Problem

Large requests retain input-only data across long responses: readBody listeners keep the original network chunks after concatenation; handleRequest passes the raw Buffer to rememberPluginMessages after forwarding; and the overflow-refold callback retains the preparation closure and parsed input.

Observed on installed 0.1.206 / 2e6f5255; the same paths remain in master bb7c249. A five-byte request reproduces leftover readBody listeners after both success and error. This establishes unnecessary retention, not the exact allocating site of a reported 4 GiB heap OOM. Buffers primarily affect external memory/RSS.

Scoped fix

  • Release readBody chunks and owned listeners on settlement, including disconnects.
  • Compute the raw-wire hash and text-comparability verdict before mutations, retaining only those scalars for bookkeeping at the existing post-forward point.
  • Release raw input and the refold preparation closure once upstream retries are finished, before response streaming.

Keep accepted outbound bodies required for fake-completion/blind retries and echo provenance, and keep the session pin through the response. No admission controls, storage changes, kernel changes, or runtime deployment.

Validation

Focused listener/abort regressions, raw-versus-precomputed bookkeeping parity, response lifecycle and overflow-retry coverage, same-session concurrency, typecheck and build.

Related but separate: #4739 / #4750 covers storage cold-load memory. #3437 / #3643 discuss broader active-request materialization; #3138 concerns oversized receive behavior, #2786 accepted-refold Prepared correctness, and #2088 fork snapshot maintenance. None implements this input-lifetime cleanup.

Topic category: N (server internals).

Activity

  1. ranxianglei commented on Oct 25, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重: 不重复。 open+closed issue/PR 按失败签名检索(readBody 监听器残留、raw buffer 跨响应保留、refold 闭包捕获)无同机制条目;issue 自身列出的 #4739/#4750(存储冷加载)、#3437/#3643(活动请求物化)、#3138(超大接收)、#2786(accepted-refold Prepared)、#2088(fork 快照维护)均为不同失败域,不并入。

    审核(复现/层次/方案):三条保留路径在 master bb7c249 全部坐实——① readBody(src/server.ts:4169)的 data/end/error 监听器从不摘除,闭包持有拼接前的全部网络 chunk,且 close-without-end 时 promise 永久悬空(admission 也随之泄漏);② handleRequest 把原始 Buffer 在 forward 之后才交给 rememberPluginMessages(src/server/handle.ts:2788),整个流式响应期间 bodyBuffer/parsed/refold 制备闭包都挂在两个挂起帧上;③ overflowRefold 闭包经 forward 参数槽贯穿整个响应流(relay.ts:1403 是最后一次使用点)。这是输入侧数据生命周期问题,不是某个分配点问题——修法方向(结算点释放 + 标量替代 Buffer)打到了根因。

    动手范围(与 issue 一致,不做准入控制/存储/kernel 改动):

    1. readBody 在成功/错误/超限/断连四条结算路径上摘除自有监听器并清空 chunk 数组;close-before-end 从「永久悬空」改为 abort 拒绝。
    2. raw-wire hash + 文本可比性判定在 prepare 任何变更之前算好,只把两个标量传到既有 post-forward 记账点(v1internal 信封展开、chain-stale 剥离都会重指 parsed,所以判定必须锚定最初解析结果——这点已核对)。
    3. upstream 重试收敛点(relay.ts:1447,所有 attempt 之后、流式之前)释放原始输入 + refold 制备闭包;出站体(wireBody)与会话 pin 保持到响应结束不动。

    验证按计划走:listener/abort 定向回归、raw-vs-precomputed 记账平价语料、响应生命周期与 overflow-retry 覆盖(现有 #1195 refold e2e 套件同时充当「过早释放会破坏 refold」的回归闸)、同会话并发冒烟、typecheck+build。完成后回 PR 链接。

  2. ranxianglei commented on Oct 25, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    PR #4766 已提交:https://github.com/ranxianglei/billion-context/pull/4766(分支 2026-10-26_input-buffer-lifetime,head 1544b5a94,推送前已合入 origin/ci-fixes 骑掉继承红)。标签:bug / scope/wiring / p2(请求管道生命周期缺陷;滞留上界 = 响应时长,非无界增长,故 p2 非 p1)。

    八问

    1. 是什么问题:大请求的输入侧内存(原始网络 chunk、原始 Buffer、解析图、refold 准备闭包)在整个长流式响应期间一直被持有,直到帧展开才释放——大请求 × 长响应时外部内存/RSS 持续占用。
    2. 什么原因导致的:三个独立的生命周期缺陷:① readBody 结算后从不摘除 data/end/error 监听器,持有 chunk 数组的闭包挂满请求整个生命周期,且 close-before-end 时 promise 永久悬挂(顺带泄漏 admission slot);② rememberPluginMessages 在转发之后才拿到原始 Buffer 重解析计算可比性判定,把整份原始 body 拖过整个响应流;③ handleRequest 帧在 await forward(...) 全程持有 bodyBuffer/bodyText/parsed 与 overflowRefold 闭包。
    3. 影响范围与严重程度:所有 plugin 车道的大请求 + 长流式响应(生成、工具循环);严重程度 = 持续性内存占用(p2)——滞留随响应结束释放、有绕行(缩短响应/重启),本 PR 按 issue 口径只确立滞留事实,不宣称与所报 4 GiB OOM 的因果链。
    4. 怎么修复的:① readBody 四条结算路径(成功/错误/中止/超限)各自摘除自己的监听器并清空 chunk 数组,close-before-end 由悬挂改为显式拒绝;② raw-wire hash 与文本可比性判定前移到 prepare 改写解析图之前(锚定原始 parse 结果、仅 plugin 车道执行),只把两个标量传到既有的转发后记账点,hash-gate 语义逐字不变;③ forward() 新增 onRetriesSettled 一次性回调,在全部上游尝试(initial send/role ladder/pck probe/image retry/overflow refold)结束、首个响应字节之前触发,handleRequest 借此释放自己的输入单元。按 issue 要求:出站 body(fake-completion/blind retry、echo provenance)与会话 pin 保留到响应结束。
    5. 是否有回归问题:逐项审过——publicSnapshotWireHash / publicSnapshotTextComparable 取值与旧算法逐字节一致(13 形状 parity corpus 对照旧算法 + e2e 断言发送字节的精确 sha256,双向 verdict 都钉住);refold 全族套件绿(issue1195/e2e-overflow-selfheal/issue3787/fix-arm-headroom-clamp/refold-wiring)+ plugin-fork 49/session-transient-memory/context-overflow 全绿;tsc + build 干净。唯一行为变更(已披露):请求体 close-before-end 由「promise 永久悬挂 + admission slot 泄漏到进程退出」变为显式拒绝 request aborted before body completed(slot 两种情况都释放)。
    6. 是否破坏缓存:不涉及——未触碰缓存/重试/去重键策略,出站字节不变,前缀缓存接缝不受影响。
    7. 是否改了设计方向:没有——纯既有请求管道内的生命周期清理;无新模块、无配置面、无 kernel/storage 改动。
    8. 是否引入新配置:无。

    判定:ready-to-review;本地定向验证全绿,全量回归以 push head 上的 CI test 车道为准(红了按 #4599 流程分账,已在该帖留一行协调记录)。

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions