Skip to content

fix: survive a corrupt unified index by degrading to legacy-only (#2771) + storage-engine proof battery - #2772

Merged
ranxianglei merged 1 commit into
masterfrom
2026-10-11_storage-engine-proofs
Oct 11, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-10-11_storage-engine-proofs

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Fixes #2771. Proof battery for the seams the #2690 coverage audit found unproven (issue #2671 refactor follow-up).

1. The bug (#2771, product fix)

A corrupt index.db (crashed checkpoint / stray write) made UnifiedStore.open throw file is not a database out of maybeOpen — SessionStore construction died, so under BILI_STORAGE_UNIFIED=full|shadow the ENTIRE proxy failed to boot, violating the sidecar's own "never break legacy" policy (record/viewFor are try/catch-guarded; construction was not).

Fix: maybeOpen now wraps UnifiedStore.open and returns null on failure (warn log, stay legacy-only). No auto-delete: the index may hold pinned checkpoint blobs (#2690 Phase 2a); the log says so.

A/B: stash the fix → the corrupt-index proof dies at construction (Error: file is not a database); with the fix → construction survives, the legacy fallback serves the byte-exact state, and writes keep working.

2. The proof battery (tests/storage-engine-proofs.test.ts)

  • WIRE PARITY ×4 wires — full-mode restart mid-conversation (keep legacy file / delete it — index-only carry) must be invisible: per-turn outbound digests equal BOTH a legacy-only restart oracle AND the committed storage-render goldens; state/section/key digests equal the legacy-restart oracle.
  • STALE REJECT — a multi-instance stale write the guarded build rejects never reaches index.db; the converged disk record is what got ingested (view digest == winning disk record digest).
  • IMPORT GUARD — a hand-aged legacy file (older savedAt than the index watermark, from before 3e17052) does not re-import over newer index state.
  • ROLLBACK — unset the env: the legacy read path serves the identical digest as the full-mode read of the same bytes.
  • SELF-DISABLE — 3 consecutive sidecar failures disable it; reads flip legacy; the legacy file stays readable.
  • CORRUPT INDEX ([Bug][storage] corrupt index.db bricks SessionStore construction in shadow/full mode (violates the sidecar never-breaks-legacy policy) #2771) — construction survives; legacy fallback digest == clean full-mode read; writes continue.
  • CAS REFCOUNT — identical block summaries intern to one row; deleteSession of one holder keeps the blob; refcount 0 collects it.

3. Findings along the way (no product change)

Config surface

No changes (BILI_STORAGE_UNIFIED semantics untouched).

Kernel boundary

No changes under kernel/.

…); pin the storage engine's restart/rollback/CAS contracts
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-11_storage-engine-proofs (fcac9e3)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.3047-pr.2772.a8b0fc6

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr2772.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review 结论:可合(CI 矩阵跑完后即满足合并条件)

审了原 issue(#2771)、PR 自身 diff、触碰路径的既有行为清单。单 commit(fcac9e3),3 文件 +505/-3,无 kernel/、无版本行、无 lockfile 噪音,diff 每一行都与目的相关。

结构化八问

  1. 是什么问题:BILI_STORAGE_UNIFIED=full|shadow 下,index.db 一旦损坏(crashed checkpoint / 盘满 / 误重定向),整个代理无法启动——直到运维手工删文件。这违反了 sidecar 自己声明的失败策略("sidecar problems must NEVER break the legacy write path")。
  2. 什么原因导致的:UnifiedShadow.maybeOpen 裸调 UnifiedStore.open;open 内 db.exec(STORAGE_PRAGMAS) 立即读文件头,损坏时抛 SQLITE_NOTADB,异常直接穿透 new SessionStore(src/persist.ts:353)。record/viewFor/importRecord 全有 try/catch + 3 次失败自禁用,唯独构造路径没守卫——问题定位到的是根因(构造缺守卫),不是表面现象(启动崩溃)。
  3. 影响范围与严重程度:仅影响显式开启 unified shadow/full 的用户,且需 index.db 已损坏这一前置条件;触发后是完全不可用(进程起不来),不是体验瑕疵。默认 legacy 用户不受影响。
  4. 怎么修复的:在决策点(maybeOpen)把 open 包进 try/catch,失败时 warn 日志(点名文件 + 给出"删文件重置"的运维指引)并返回 null → 纯 legacy-only,与加密 opt-out 同形。不做自动删除是正确取舍:index.db 可能持有 pinned checkpoint blobs(Unified SQLite storage: corpus pins + parity core + shadow dry-run + drop-in read path (#2671 Phases 1 & 1.5) #2690 Phase 2a),破坏性恢复是操作员策略。这是根因修复而非打掩码——没有去 startServer 层兜底吞错,也没有在传输层改写症状。
  5. 回归审查(逐项过既有行为):
    • maybeOpen legacy 模式返回 null:不变 ✓
    • 健康 index 的 open 路径(sidecar 实例化 + info 日志):不变 ✓
    • record/viewFor/importRecord/自禁用阈值(MAX_CONSECUTIVE_FAILURES=3):未触碰 ✓
    • 损坏后的读回退走的是既有 "unified miss → legacy fallback" 语义(shadow=null 即纯 legacy 路径),没有新造分叉 ✓
    • src/export.ts:322(bili export --jsonl)仍裸调 UnifiedStore.open,损坏时会抛错——核查后判定符合设计:显式操作者路径本就该 throw(shadow.ts Phase 2 pass-throughs 同一纪律),不在本 issue 范围,不改 ✓
    • tests/golden-storage-drive.ts:改动为纯增量可选 opts,现有调用方(golden-storage-render.test.ts)默认行为不变;finally 新增 BILI_STORAGE_UNIFIED 保存/恢复,严格更 hermetic ✓
    • 高危模式逐项:无语义漂移、无 fail-fast 变吞错(要消除的 fail-fast 正是构造路径这条;显式路径的 throw 保留)、无阈值/时序/输出格式变化 ✓
  6. 是否破坏缓存:不涉及(CAS refcount 语义反而新增了钉死测试,行为未变)。
  7. 是否改了设计方向:没有。失败策略原样保留,只是补上构造这一段;配套 proof battery(wire parity ×4 线 ×keepFile/dropFile 两种重启形态、stale reject、import guard、rollback、self-disable、corrupt index、CAS refcount)是把 Unified SQLite storage: corpus pins + parity core + shadow dry-run + drop-in read path (#2671 Phases 1 & 1.5) #2690 审计认定的未证明缝钉成合同,pure proof、零产品变更。PR 正文 §3 的三个发现(bs/bl/be 重启固有缺口、lastMessages 一次性尾序归一、flushAll 关闭 sidecar)处理得当——都定性为诊断级并明确"持久化 seam slots 属产品决定",没有顺手改产品。
  8. 是否引入新配置:无。BILI_STORAGE_UNIFIED 语义未动(config-surface-guard 绿)。

行为变更披露(按交付规则第 4 条):旧行为 = 损坏 index.db → 进程启动失败;新行为 = warn 日志 + 降级 legacy-only,读写走 legacy 路径正常。这就是本修复本身,旧值是缺陷不是特性。

测试有效性:corrupt-index 腿逐字节复现原 bug(前 512 字节写 0x5a → SQLITE_NOTADB),断言三件事都有牙:构造存活、legacy 回退 digest 与干净 full-mode 读逐字节相等、写继续工作;A/B 声明(去掉修复则测试死于构造)与代码路径一致。wire parity 的双 oracle(committed goldens + legacy-restart)和 dropFile 腿(删光 legacy 文件后索引必须自足)是真断言,非恒真。hermetic 隔离(BILI_CONFIG_FILE 指向空配置 + env 保存/恢复)、mkdtemp、慢腿带超时,确定性没问题。

§7.4 auto-merge 门禁:单模块修复 + 复现原 bug 的回归测试 + 不触 config/persistence/wire/kernel/版本 + 干净 diff + Fixes #2771 + 不碰承重设施——条件齐备。

CI:全部 guard 作业绿(kernel/lane/config-surface/lock-sync/docs-sync/version/pi-subagents/docs-coverage/build-artifact/unused-exports);test 矩阵(ubuntu/windows × node 22/24)仍在跑,转绿后我跟进确认。

可合。

@ranxianglei
ranxianglei merged commit 690f7c7 into master Oct 11, 2026
12 of 14 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.278-master.690f7c7 (rolling master tag).

npm install -g billion-context@0.0.278-master.690f7c7  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

ranxianglei added a commit that referenced this pull request Oct 11, 2026
…-fix

test(storage): close index.db handles before rmrf — fix Windows EBUSY in storage-engine-proofs (#2772 follow-up)
YunzhouLi-hub pushed a commit to YunzhouLi-hub/billion-context that referenced this pull request Oct 11, 2026
…nxianglei#2772 follow-up)

The four engine-proof tests that run under BILI_STORAGE_UNIFIED=shadow|full
leaked their SessionStore shadow handles (cancelAll only cancels timers; it
does not close the SQLite connection), so the rmrf(tmpdir) cleanup retried
EBUSY for ~3s and then failed on windows-latest (Node 22/24). POSIX unlink
tolerates open handles, which is why Linux never saw it.

- loadFolded: cancelAll -> closeQuietly (the loader store is discarded right
  after loadSync; this was the leak shared by all four failing tests)
- end-of-test read-only stores in the import-guard/rollback/corrupt legs:
  cancelAll -> closeQuietly
- kept cancelAll at the corrupt-fallback read that is followed by writeNow
  (mid-test: the store must stay usable; its own closeQuietly follows at the
  end of the test)
@ranxianglei
ranxianglei deleted the 2026-10-11_storage-engine-proofs branch October 15, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug][storage] corrupt index.db bricks SessionStore construction in shadow/full mode (violates the sidecar never-breaks-legacy policy)

1 participant