Repository navigation
[PR] feat(storage): async reload seam + bounded DB pre-warm for network backends - #4987
ranxianglei wants to merge 13 commits into
Conversation
…ckends (#4942) MySQL-backed unified storage had no synchronous read facade: viewFor() returns null and listSessionIds() returns [] on network backends, so every hot-path read (cold load after restart, LRU eviction reload, boot enumeration) fell back to local JSON envelopes. Any deployment that stops writing envelopes lost every session at every process restart. Owner decision on #4942: option A (async pre-warm + async reload seam), keeping the byte-bounded resident pool intact. - SessionStore.loadAsync: envelope/index-first (zero behavior change whenever the sync path serves), DB fallback only when the sync read misses AND the sidecar is an async backend; awaits ready() then viewForAsync(). - getSessionAsync: single-flight variant of getSession for async call sites (concurrent first-touches share one load); main request entry in server/handle.ts switched to it. - boot/bootBounded/loadAll unions now enumerate DB ids via sessionSummariesAsync() (newest-first under the existing maxSessions/maxBytes budget) when a network full backend is expected; sqlite/file paths unchanged. - resolveForkConversation / publishForkSession / maybeAdoptForkBlocks / reconcileDerivedLineage / handlePluginStatus made async (all callers already in async contexts). - UnifiedShadow.fromBackend factory + exported recordToLegacyView for hermetic testing against stub backends. Deliberate sync boundaries left as-is (documented in PR body): hasProcessedState, resume-inheritance parent probe, decompress chain walker. Tested: new hermetic tests/storage-mysql-seam.test.ts (typed stub implementing the full StorageBackend contract, no DSN required). Related pre-existing ingest-verify finding filed separately: #4986.
…ane repair) The new seam suite shipped red at the PR head (verified against the pre-rebase commit): 8 tsc errors from a top-level 'blocks' access that does not exist on Session (blocks live under state), and two over-strict reference-identity assertions between separate cold loads. - .blocks -> .state.blocks (4 sites): mechanical API alignment; makes the typecheck lane pass. - assert.equal(viaAsync, viaSync) / assert.equal(b, a) -> deepEqual (2 sites): every cold loadSync builds a fresh Session object (no cross-call caching), so reference identity was never part of the contract - even loadSync-vs-loadSync differs. The real contracts stay pinned: envelope precedence, zero extra DB reads while an envelope exists (the viewCalls guard is unchanged), and no-sidecar parity of loadAsync over loadSync.
c906dea to
72c61b6
Compare
📦 Built Package ArtifactBranch: Latest build — install this exact versionnpm install -g billion-context@0.0.7191-pr.4987.860fde1This exact version is immutable and reproducible. The rolling Recent builds (commit → version)
Download artifact
tar xzf billion-context-pr4987.tgz
npm install -g packageInstall instructions are refreshed on each push. |
(ai: vllm-qwen/qwen3.8-27b) 冲突已解决:分支已 rebase 到最新 master(9b6ec23)并强推,head = 72c61b6。 八问
附带发现与处置(测试契约变更披露)验证时发现本 PR 自带的新 seam 套件在 rebase 之前就是红的(用独立 worktree 对照原 head c906dea 实测,失败签名完全相同,非合并引入):
该修复是独立标注 commit(72c61b6,仅动 验证证据:seam 套件 7/7 绿;全量 typecheck 干净;定向套件(fork-ccr-adoption、storage-json-writer-off、plugin-fork、fork-adoption/wires/plan)合计 67 过 0 挂;全量回归交给 CI。推送时 GitHub API 配额耗尽无法直读 mergeable 字段,已用 git 确认 rebase 后 master 未再前进(仍为 9b6ec23),分支正基于当前 master tip,merge 状态会随 force-push 自动转干净。 相关:#4942 分析途中发现的存量缺陷(DIGEST DRIFT 误报 + 全尾重灌)已单独立项 #4986(p2,已有会话在处理),与本 PR 无关、不阻塞合并。 判定:冲突解决完毕,分支基于最新 master、本地定向验证全绿,可等 CI 全局把关后合并。 |
resolveForkConversation probed with loadAsync but materialized with the sync getSession. On a network storage backend a DB-only conversation id (no local envelope/index copy) then minted an EMPTY resident ghost, and that ghost poisoned every later getSessionAsync of the same id for the process lifetime — plugin status/fork/snapshot surfaces operated on an empty session and fork children inherited zero blocks (#4942 seam). Materialization now rides the same async seam as the probe. Regression: the seam suite drives handlePluginStatus for a DB-only corpus session and asserts the resident pool holds the DB state (fails on pre-fix code).
…4987 CI) #4942 made handlePluginStatus() and reconcileDerivedLineage() async (and maybeAdoptForkBlocks()/publishForkSession() in the same seam). Six existing suites still called them synchronously and asserted on the response/log before the microtask completed, red across 7 of 9 CI shards: - tests/issue404-restore-timestamps.test.ts: JSON.parse of the empty mock body - tests/advisory-panel.test.ts, tests/update-notes.test.ts, tests/issue2029-status-baseline-provenance.test.ts: 0 !== 200 on the mock res - tests/lineage-reconcile-boot.test.ts (AC10): 'not on disk' log asserted before it was written - tests/fork-ccr-adoption.test.ts: same class, passed only because the parent stayed resident (sync completion by luck) — aligned too
(ai: vllm-qwen/qwen3.8-27b) CI 已修复并推送到 PR 分支: 八问
另注:windows 22-1 上还有一个 判定:修复已推送,等 CI 重跑;全绿后即可进入人工 review/合并。 |
…ync-reload-seam # Conflicts: # src/plugin.ts
|
(ai: vllm-qwen/qwen3.8-27b) Merged origin/master into this branch — one textual conflict in |
|
(ai: vllm-qwen/qwen3.8-27b) Re-synced origin/master into this branch — clean auto-merge, no conflicts (master picked up #4982 draining-twin fix in src/server.ts, disjoint from the storage seam); local typecheck clean, pushed head |
(ai: vllm-qwen/qwen3.8-27b) Merge-guard re-sync done: merged |
…ync-reload-seam # Conflicts: # src/plugin.ts # src/session.ts
…uires (#4599) The #5140 recovery test proves forkSnapshot reconstructs a non-retained parent's raw tail from the UNIFIED index — but it never pinned its own storage mode. Under the CI storage baseline (preload-storage-baseline.mjs sets BILI_STORAGE_UNIFIED=legacy for the broad corpus) no unified rows exist, so the reconstruction has no source and forkSnapshot fails closed with 409 SNAPSHOT_UNAVAILABLE. Deterministic, not a flake: reproduces 100% on master and any merged branch with BILI_STORAGE_UNIFIED=legacy, passes without it (default shadow). Pin full like the sibling persist tests and restore the ambient value afterwards.
# Conflicts: # src/persist.ts # src/web/sessions-data.ts
…m-test stub with the current StorageBackend Merge of ci-fixes into the PR head surfaced two interface drifts against current master: - src/web/sessions-data.ts loadSessionById called getDiskStore(), a local SessionStore accessor that master (#4990) deleted in favor of the persist process singleton getStore(). Keep the PR's async method (loadAsync) on the master accessor — a locally constructed store re-ran the open-time path on the live main thread, which is exactly what #4990 removed. - tests/storage-mysql-seam.test.ts StubMysqlBackend now implements the two StorageBackend members added on master (sessionViewRows, readSnapshot) and honors the read options the real backends apply (seqs filter on getMessages, limit/offset paging on getBlocks, tail-pointer filtering in sessionViewRows) so viewForAsync batches against the stub exactly as it does against sqlite/mysql.
(ai: vllm-qwen/qwen3.8-27b) Synced origin/master into this branch — this round merged cleanly with zero conflicts (the branch already carried the ci-fixes convergence + seam-singleton fix from |
[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) Merge-guard 状态收口(本轮无新冲突,补一次终态确认):
判定:PR 与 master 同步完毕、无冲突、本地定向全绿,等 CI 在新 head 上跑完即绿即可由 owner 合并。无其他待办。 |
(ai: vllm-qwen/qwen3.8-27b) CI 自动修复进度汇报(进程曾被看门狗重启,现恢复并汇报): 已修复并验证
当前剩余(非本 PR 引入)
八问:
结论:除继承自 master 级的 win22-1 存储 flake shard 外分支已绿;最后一次 rerun 观察后即以 #4599 为准移交。 |
[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) 收到,确认收敛完毕。已在 master(e193861)核实本 lane 的两处 CI 修复均随 #5213 落地:
遗留一项已留档 #4599:Windows 上 issue4739/issue5103 存储测试的 flake 簇(EBUSY unlink / pending-promise 挂起,master 自身亦曾 6 片红同族签名)——建议哨兵在 ci-fixes 收敛(tmp-rm EBUSY 重试 / teardown close 顺序)。本 lane 工作至此结束。 |
Closes #4942
What
Owner decision in #4942: option A — async pre-warm at boot + async reload seam for network backends (mysql), keeping the byte-bounded resident pool intact (no LRU-eviction disable).
Before this change the MySQL unified store had no synchronous read facade (
viewFor()→ null,listSessionIds()→ [] on network backends), so every hot-path read fell back to local JSON envelopes; a deployment that stopped writing envelopes lost every session at every process restart while the data sat intact in MySQL.Mechanism
SessionStore.loadAsync(id, meta?)— envelope/index-first (zero behavior change whenever the sync path serves); only when the sync read misses AND the sidecar is an async backend does itawait ready()→viewForAsync(). Envelope precedence is deliberate: envelope writes are synchronous per save while DB writes ride the coalesced queue, so the envelope is never older than the DB for a single writer.getSessionAsync(id, meta?)— single-flight variant ofgetSession(concurrent first-touches share one load via a per-id promise map); the main request entry insrc/server/handle.tsnow uses it.boot/bootBounded/loadAllunions now enumerate DB ids throughsessionSummariesAsync()when a network full backend is expected, sorted newest-first so the existingmaxSessions/maxByteswarm budget keeps the hottest sessions; sqlite/file paths are byte-identical to before.resolveForkConversation,publishForkSession,maybeAdoptForkBlocks,reconcileDerivedLineage,handlePluginStatusbecame async; every existing caller was already in an async context.UnifiedShadow.fromBackend(mode, backend, legacyLoad)factory (does not register into the sidecar instance registry) and exportedrecordToLegacyViewlet the hermetic suite drive a real sidecar against a typed stub.Behavior changes (old → new)
null→ fresh empty session (the data-loss class of MySQL-backed storage loses all sessions after a restart when JSON envelopes are off (no synchronous read facade) #4942 when envelopes are off); now awaits the DB row. Cost: +1 DB round-trip on FIRST TOUCH ONLY; zero latency change whenever an envelope/index serves (that path is tried first and untouched).sessionWarmMax*limits that bound sqlite boot.Deliberate sync boundaries left as-is (same mechanism family — checklist, not new issues)
hasProcessedState(src/session.ts) — conservative exemption predicate for chain-detection passthrough; failure mode is mis-exemption, not loss; the memory map covers it after first touch.loadParentclosure, src/server/handle.ts) — feeds sync predicate interfaces; only affects first-request fork-resume inheritance on writerOff-mysql, whereloadSyncmissed before too — no regression vs today.derivedAncestorSessions(src/decompress-shared.ts) — decompress/search_context parent-chain fallback degrades until an ancestor is touched; same behavior as today's miss.Tests
New hermetic suite
tests/storage-mysql-seam.test.ts(7 cases, typed stub implementing the FULLStorageBackendcontract, no DSN required, golden corpus):loadSyncnull (old contract pinned) +loadAsyncserves the row with identical blocks;loadAsync≡loadSync;boot()pre-warms the DB-only id within budget, blocks intact;loadAll()unions in the DB-only id;getSessionAsyncreloads into the resident pool; concurrent first-touches share ONE object (single-flight);BILI_STORAGE_BACKEND=mysql+unified=full+jsonWriter=offkeeps jsonWriterOff=false with the documented warning (envelope still written; C-now policy of MySQL-backed storage loses all sessions after a restart when JSON envelopes are off (no synchronous read facade) #4942).Local targeted runs green (seam suite 7/7,
tests/storage-json-writer-off.test.ts), typecheck clean, build OK; full-suite verdict = CI.Related finding (separate issue)
While making the stub faithful I reproduced a PRE-EXISTING ingest-verify defect on stock master (no local changes involved): error-level
DIGEST DRIFT+ watermark drop + full-tail re-ingest for sessions whose #4316-aligned in-memory byRef key order diverges from stored refs-row order. Self-healing, no data loss — filed separately as #4986 (p2). The seam suite documents it as expected noise rather than asserting on it.No config surface changes. No kernel/ changes. No version bump.