Repository navigation
[PR] fix(#5219): bound the bounded-boot parse window by decoded-file count - #5634
ranxianglei wants to merge 4 commits into
Conversation
The #3199 parse window stops when candidates >= maxSessions AND bytes >= maxBytes. When neither term binds — maxSessions=0 (the sessionWarmMax() default) or many small files under a large byte budget — the loop decoded the WHOLE corpus synchronously pre-listen, then popped every candidate in the savedAt-desc trim anyway. On a store that grows to thousands of files this exceeds the dsh launcher's 60s health budget; the hung child keeps the index.db write lock, the launcher kills without confirmed death, and the next spawn poisons off the lock — the #5219 spawn-kill loop. Decode attempts are now capped: 0 when nothing survives the trim (the default config), otherwise min(2048, max(64, 16*maxSessions)) so normal corpora never hit it (the byte budget stops first there). Files past the cap stay lazy-loadable through loadSync; their alias bookkeeping lands in scanRemainderInBackground, which already owns the out-of-window files. Warm-set semantics are unchanged in every case where the cap does not bind; at maxSessions=0 the warmed set was empty before and after, so only the wasted pre-listen work moves to post-listen. Gate: tests/issue5219-bounded-boot-decode-cap.test.ts (red on the old loop: 100/70 decodes pre-listen; green with the cap).
📦 Built Package ArtifactBranch: Latest build — install this exact versionnpm install -g billion-context@0.0.9317-pr.5634.6db358eThis exact version is immutable and reproducible. The rolling Recent builds (commit → version)
Download artifact
tar xzf billion-context-pr5634.tgz
npm install -g packageInstall instructions are refreshed on each push. |
(ai: vllm-qwen/qwen3.8-27b) 查重结论:不重复。同机制族最近的前置工作是 #4515(boot 全量扫描历史语料撞 60s 冷启动预算)及其 Slice-1 PR #4539——两者均 closed 未合并(孤儿尝试);本 PR 完成同一方向并 supersede 它们。无其他 open issue/PR 触碰 标签:bug / dsh / scope/storage / p1(dsh launcher 确定性 spawn-kill 循环、用户不可用;根因在会话存储 boot 解析路径——原 issue #5219 的 triage 标签 scope/wiring 按已确认根因应为 scope/storage)。 代码审查(diff 逐行 + 既有行为回归清点 + 测试有效性)已完成,要点:
head |
(ai: vllm-qwen/qwen3.8-27b) PR #5634 review 结论(独立重做:新旧代码逐行、v0.1.209 标签核实、既有行为逐项清点、配置默认值与唯一调用点验证) ① 是什么问题:统一存储模式下默认配置的冷启动在 listen 前同步解码整个会话语料(直到 64 MiB 字节预算),事件循环被堵死 → dsh launcher 60s 健康预算耗尽 → spawn-kill 循环 → 孤儿子进程持有 index.db 写锁毒化下一次启动 → ② 什么原因导致的: ③ 影响范围与严重程度:p1,用户不可用。触发条件:unified 存储(shadow/full)+ warm 配置使窗口退化(默认即命中)+ 语料大到 pre-listen 解码超过健康预算余量。报告者 store ~3.5h 使用量即命中;小语料用户无感(解码快)。legacy(非统一)分支不受本修复覆盖——有意保留的残余边界(见⑤)。 ④ 怎么修复的:给窗口加第三个停止项——已解码文件数上限: ⑤ 是否有回归问题:diff 触碰路径的既有行为逐项清点——
⑥ 是否破坏缓存:不涉及。boot warm set 只影响内存驻留与首次触达延迟;模型线 prefix cache、压缩状态、去重键、wire 内容均未触碰。 ⑦ 是否改了设计方向:没有。完全留在 #3199「窗口 + 延迟」架构内,只是让窗口在两个停止项都不绑定时也遵守自己的契约。 ⑧ 是否引入新配置:没有。仅三个内部守护常量,无 env/config/CLI 面(PR 的 Config surface 声明属实,已独立核实)。 测试有效性:gate 三个 case 都有牙——退化配置(旧码 100 次 pre-listen 解码 → RED,修后 <10)、capped 语料 70 文件/maxSessions=4(旧 70 → RED,修后 64≤n<70)、小语料对照(恰好 10==10,钉住 cap 不绑定时零行为变化)。计数走实例级 diff 整洁度:单 commit、2 文件、+164(21 src / 143 test),每行都为目的服务;无空白噪音、无跑题文件。代码质量达标:具名常量、注释讲机制与出处(# 引用齐全)、风格与周边一致。 CI 状态(合并前置):head 判定:代码审查通过——根因定位到机制层、修法最小且尊重既有延迟契约、行为变更完整披露、测试有牙、diff 干净。待 CI 重跑绿即可合(原 issue 已挂「马上合并」,建议重跑后直接放行)。 |
(ai: vllm-qwen/qwen3.8-27b) 补充:CI 状态同步(代码结论不变)。 发生了什么:第一遍 synchronize 运行里,test 矩阵中途被取消(约 02:38Z),没有留下任何结论;最终跑完的只有 12 个 guard 车道(kernel-guard / lock-sync / version-guard / config-surface-guard / docs-sync / docs-coverage / lane-guard / pi-subagents-guard / unused-exports / lifecycle-ledger-guard / test-import-graph / plan,全绿)。也就是说:全量测试的裁决目前还不存在,不能直接合。 我做了什么(只动 PR 分支,零代码改动、零冲突):
新 head 判定不变:代码审核通过(见上一条八问);CI 转绿后即可合并。 |
(ai: vllm-qwen/qwen3.8-27b) CI 裁决出来了:全绿,可以合并。 同步后的 head
注:镜像缓存一度停留在旧 sha 的 pending 快照,上面结论取自对 head sha check-runs 的单次直读(非轮询)。 收工判定:代码审核通过(八问见前)+ base 已同步到当前 master + 全量测试绿 → 本 PR 可合。合并动作留给你(owner-only)。合并后 #5219 可以关单验收。 |
ranxianglei
left a comment
There was a problem hiding this comment.
(ai: vllm/qwen3.8-27b)
APPROVE — 可合。 单模块修复,#5219(p1 冷启动崩溃循环)的根因修复,回归门到位,行为变更已披露。
结构化八问:
-
是什么问题:默认配置下,每次冷启动在 proxy listen 之前把整个会话存储同步解码一遍。会话文件涨到几千个时,打爆 dsh 启动器的 60s 健康预算 → 子进程被杀但没确认死、还攥着 index.db 写锁 → 下一次 spawn 被这把锁毒化 → spawn-kill 死循环,最后
fatal load failure(#5219,p1)。 -
什么原因:根因在 #3199 bounded-boot 解析窗口的停止条件
candidates >= maxSessions AND bytes >= maxBytes—— 两个条件都不绑定时退化:maxSessions=0(sessionWarmMax() 默认值,计数项恒真)或「大量小文件 + 大字节预算」时,循环在 listen 前把每个文件都解码完,随后 savedAt-desc 修剪又把它们全 pop 掉,纯浪费的同步工作随语料规模线性增长。 -
影响范围与严重程度:p1。任何会话存储长大的安装都会中(报告者 ~3.5h 内从 0 涨到触发挂起);dsh/pi 等 launcher 用户每次冷启动都挂,服务整体不可用。小存储的正常安装不受影响(字节预算会先停住循环)。
-
怎么修:给窗口加一个「已解码文件数」上限,与既有计数/字节项并列:maxSessions=0 时为 0(反正一个都活不过修剪),否则
min(2048, max(64, 16×maxSessions)),正常语料永远撞不到(字节预算先停)。超上限的文件仍走 loadSync 懒加载,其别名记账交给本就存在的 listen 后 scanRemainderInBackground。快速恢复 listen,记账合作式后置。 -
回归风险:逐条过 boot 窗口承担的既有行为——warm 集仲裁(savedAt-desc)、字节预算、legacy 全量解析分支、listen 后余量扫描、loadSync 懒加载——全部保持。唯一变化是 pre-listen 解码次数被封顶:maxSessions=0 时 warm 集前后都是空(不变);maxSessions>0 且语料小于上限时完全一致。边角(PR 已披露):语料大于上限时,warm 仲裁只看 mtime 最新的 cap 个文件而非全量,某个 savedAt 比 mtime 位置更新、却排在 cap 之外的文件会从「预热」降级为「按需加载」(可用性保留)。无 fail-fast 变吞错、无阈值/时序/输出格式漂移(除已披露的上限外)。
-
是否破坏缓存:不涉及——没碰任何缓存/重试/增量游标/去重键策略,只动 boot 预热集大小。
-
设计方向:未改。仍落在 #3199 的延迟设计里(mtime 定序窗口、savedAt 仲裁存留、余量 listen 后扫),只是把退化情形下的窗口收紧到设计本意。
-
新配置:无。只有三个内部具名常量(
BOOT_WINDOW_DECODES_PER_SESSION/MIN/MAX),没有新 env/config/CLI 字段。
验证:根因链对着 PR head 代码核过(sessionWarmMax() 默认 0 @ src/knobs.ts:420;initSessions 传 maxSessions=0 @ src/session.ts:1291;bootBounded 窗口循环 @ src/persist.ts:964)。本地在 rebase 后的 head 跑了新门 3/3 + 相邻 bounded-storage / bounded-storage-scale / issue3199-overview-cold-scan 共 15/15 绿;全量判定以远程 CI 为准(lane 状态为绿)。diff 相对 master 只多 src/persist.ts(+21)与新测试(+143),wire-exit-matrix.test.ts 那处来自 ci-fixes 合并,非本 PR 贡献。
判定:可合,满足自动合并门槛(§7.4):单模块修复、回归测试红转绿、不碰 kernel/wire/config/持久化格式、diff 干净、引用 #5219。
(ai: vllm-qwen/qwen3.8-27b) ✅ review-worker-r1 独立复审完成(非照抄前序,自己重做一遍):重读修复 diff → 对着 PR head 代码核根因链( 判定:APPROVE / 可合。 单模块根因修复(给退化窗口加 decoded-file-count 上限)、回归门有牙(旧码 100/70 pre-listen 解码 → RED)、行为变更已按规范披露、不碰 kernel/wire/config/持久化格式、diff 相对 master 只多 2 文件(wire-exit-matrix 那处来自 ci-fixes 合并,非本 PR)。无顺手可修的小问题——代码已达标(具名常量、机制注释、风格一致)。 说明:GitHub 侧的正式 APPROVE 状态因「不能 approve 自己的 PR」(当前凭据即仓库属主账号)被平台拦下,无法置为 approved;已改以完整结构化八问评论形式落在 PR #5634(review id 5763091646),判定内容完整在案。合并动作留给你(owner-only);合并后 #5219 即可关单验收。 |
(bot) 🏷 冲突已解决并推送(attempt 1/3)。本条按七问汇报。 核对说明:镜像线程标注的上游号 5599 实为该 PR 引用/关闭的 issue 编号;真正的上游 PR 是 #5636(head 做了什么:把分支 rebase 到当前 master( 七问
当前实况(推送后即时核实):GitHub |
[ework] CI fix verified green ✓ |
[ework] CI fix verified green ✓ |
3 similar comments
[ework] CI fix verified green ✓ |
[ework] CI fix verified green ✓ |
[ework] CI fix verified green ✓ |
…lict Took master's lag-aware teardown budget (ci-fixes c37293b) for tests/wire-exit-matrix.test.ts — a strict superset of this branch's win32-scoped constant: same ABORT_TEARDOWN_BUDGET_MS (win32 20s / else 10s) plus the event-loop-lag extension (teardownPollContinues, TEARDOWN_POLL_MS) and its arithmetic test. No intent from either side lost. src/persist.ts auto-merged clean (decode-cap fix intact).
[bot] 已解决 PR #5634 与 master 的合并冲突并推送(merge 方式,fast-forward,无 force)。 This run (English): Merged current master into the PR branch, resolved the single conflict in 七问(中文):
判定:冲突已清除,PR 处于可合并(mergeable / behind)状态,可进入正常 review/merge。 |
Fixes #5219
What breaks
dsh-launcher report ("service crashed, sessions exploded instantly"): every cold start hangs pre-listen, exceeds the launcher's 60s health budget, gets killed without confirmed death, and the surviving child's index.db write lock poisons the next spawn — a spawn-kill loop ending in
fatal load failure. Reporter's v0.1.209 log: each boot logs ONLY the storage-init line and never reaches[persist] indexed N state files.Root cause
The bounded-boot parse window (
SessionStore.bootBounded, src/persist.ts) stops whencandidates >= maxSessions AND bytes >= maxBytes. When NEITHER term binds, the loop degenerates:maxSessions = 0is the DEFAULT (sessionWarmMax()): the count stop-term is vacuously true, so the loop decodes the WHOLE corpus synchronously pre-listen until the 64 MiB byte budget — then pops every candidate in the savedAt-desc trim anyway (nothing survives at warm=0). Pure wasted work, scaling with corpus size.maxSessions > 0, a corpus of many small files fills the byte budget one file at a time — same unbounded decode count.The reporter's store went from 0 files to hang-inducing within ~3.5h of use; every cold start since exceeded the health budget.
Fix
Cap the window by DECODED-FILE COUNT, alongside the existing count/bytes terms:
maxSessions === 0→ 0 decodes (nothing survives the trim regardless); alias/canonical bookkeeping falls back toscanRemainderInBackground(post-listen, event-loop yielding) +seedIndexedAliases(sync, index-based, no decode) — the original Web UI first paint stalls 78-180s+: /__bili/overview cold scan blocks the event loop, and the freeze itself feeds the respawn churn #3199 deferral contract.min(2048, max(64, 16*maxSessions))— normal corpora never hit it (the byte budget stops first there); pathological many-small-files corpora are bounded.Files past the cap stay lazy-loadable through
loadSync; nothing on disk is touched or deleted. The legacy (non-unified) branch keeps its deliberate synchronous full parse — documented residual boundary.Behavior change (disclosed)
sessionWarmMax() = 0): warmed set UNCHANGED (empty before, empty after); pre-listen synchronous decode work goes from "whole corpus up to 64 MiB" to ZERO. Boot latency on large stores drops from minutes to ~0; no data-path change.maxSessions > 0on corpora LARGER than the cap: warm-set arbitration now sees the newest-by-mtimecapfiles instead of the whole corpus; a file whose savedAt outranks its mtime position past the cap degrades from warmed to lazy-on-demand (availability kept, warmth lost in a corner case). Corpora below the cap (every normal install) behave identically.Config surface
None — internal guard constants only; no new env/config/CLI fields.
Tests
tests/issue5219-bounded-boot-decode-cap.test.ts: three deterministic cases — degenerate config (old code: 100 pre-listen decodes → RED; fixed: <10), capped corpus (old: 70 → RED; fixed: 64–69), small-corpus control (10 == 10 on both sides, pins no behavior change when the cap doesn't bind).Related: #3199 (window origin), #404 (warm semantics), #2803 (decoded-byte budget), #4539/#4515 (earlier orphaned attempt), #5294 (companion second-writer fix, already shipped in v0.1.210+).