Skip to content

service-storage: StorageMetadataStore 把每一次引擎写入失败都静默吞掉并退回进程内存 —— sys_file 的「business truth」可能凭空消失且无一行日志 #5216

Closed

Description

@os-zhuang

在 #5202(把 sys_upload_session 加进 plugin-audit 的 SKIP_OBJECTS)里核对写入面时发现。不在 #5202 的 PR 里修(越界,一事一议;那单只动 packages/plugins/plugin-audit/src/)。

事实(对 origin/main @ b8503183c 核过)

packages/services/service-storage/src/metadata-store.ts 里,StorageMetadataStore 的全部 8 处引擎调用都套在同一个形状里 —— try { await this.engine.X(...) } catch { /* ignore */ },catch 块里没有任何 logger 调用,异常不外抛:

方法 行 调用 catch 注释
createFile() :83 engine.insert('sys_file', full) engine not available or schema not migrated — keep in-memory only
getFile() :94 engine.findOne('sys_file', …) fall through to memory
updateFile() :110 engine.update('sys_file', …)
deleteFile() :122 engine.delete('sys_file', …)
createSession() :146 engine.insert('sys_upload_session', full)
getSession() :157 engine.findOne('sys_upload_session', …)
updateSession() :178 engine.update('sys_upload_session', …)
deleteSession() :190 engine.delete('sys_upload_session', …)

每个写方法在调引擎之前都先写了进程内的 private readonly files = new Map(...) / sessions = new Map(...)(:68-69),读方法则「先引擎、失败或未命中就退回 Map」。类注释(:60-66)把这个 fallback 说成:

Backed by IDataEngine (objectql) when available — otherwise falls back to a process-local Map (suitable for tests and dev environments where the data engine isn't wired up).

问题:声明的意图与实际覆盖面不一致

注释和 catch 里的措辞说的是引擎不存在 / schema 未迁移这两种启动期状态。但 if (this.engine) 已经把「引擎不存在」挡在外面了,所以这些 catch 实际捕获的是引擎已接好之后的运行期失败 —— 约束冲突、连接抖动、RLS 拒绝、驱动错误。这类失败被:

  1. 不上报 —— 调用方拿到的是正常返回值(createFile 返回 full,updateSession 返回 merged),HTTP 层照样 200;
  2. 不记录 —— catch 块里没有 logger,this.engine 也没有被标记降级;
  3. 被内存 Map 掩盖 —— 紧随其后的 getFile() / getSession() 会从 Map 里读到那条「以为写成功了」的记录,所以同进程内自检也看不出异常。

后果按对象分两档:

为什么单独立单

我个人倾向 A:if (this.engine) 已经区分开了「没接引擎」与「接了引擎但写失败」,后者静默吞掉是把一个可诊断的错误变成一份丢失的业务真相,而 fallback 声称服务的对象(tests / dev)恰好是 this.engine 为 null 的那条分支 —— 也就是说这些 catch 对它们声称的用途并不必要。

未验证

我只做了静态阅读,没有构造一次真实的引擎写失败来观察端到端表现,也没有查是否有更上层的重试/补偿逻辑兜底。严重度请 PM 判。

Found-during: #5202 / PR #5215

Activity

  1. added
    bugSomething isn't working
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 4, 2026
  2. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    分诊(PM 循环,#5202 派生):auto-queue —— pm:queue + domain:services。8 个 engine 调用全包在无日志的 catch { /* ignore */ } 里、createFile 失败静默丢 sys_file 业务真相而 API 返回 200,这是本线一路在消灭的静默降级形态,方向由仓库既有立场决定(AGENTS.md「absence must be loud」、durability 失败钉 error),不需要维护者拍板方向;三个修复选项里选哪个在派发时由 PM 读完正文后裁定并写进派发词,dev 可反驳。预设倾向:承载业务真相的写入(createFile / complete 一类)失败应响亮失败而非降级,Map 回退只保留给 engine 尚未接线的启动窗口;纯缓存性读写可降级但必须打 error。排下一轮批次。


    Generated by Claude Code

  3. self-assigned this
    on Aug 4, 2026
  4. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 9 轮
    会话:session_017MCKJaEomEqg4tvz4SzdNd
    分支:claude/issue-5216-storage-metadata-loud-failure
    Worktree:objectstack-issue-5216
    域:domain:services
    文件面:packages/services/service-storage/src/(越界即停,报告说明)

    PM 裁定(三选一里选 A,带一处细化;实现可反驳):


    Generated by Claude Code

  5. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    复核结论:ACCEPT —— PR #5232,转 ready 并入合并队列。head 上 24 个 check run 全部 success/skipped。

    裁定执行情况与两处 dev 的独立判断,均采纳:

    1. 纠正 PM 笔误:实际 6 写 / 2 读(我派发词写成 5/3),issue 正文的 8 行清单是准的。
    2. 读路径判抛而非「error 后回退」:全部 6 个调用方拿到 null 一律回 404 —— 静默回退等于把引擎故障翻译成「文件不存在」,GET /files/:id/url 在 outage 下回 500 而非 404 的用例直接钉住了这个区别,miss 仍走 404 不变。
    3. Map 残影语义:引擎在场时根本不写 Map,残影在结构上不可能出现 —— 比「写后回滚」干净,且让类注释声称的 fallback 服务对象(engine 未接线)第一次成为事实。
    4. 上层零适配:storage-routes 的 handler 本就 catch → 500,抛错自然落成正确行为,有 HTTP 层测试为证;全仓唯一构造点在 storage-service-plugin。
    5. 两个新假引擎的 delete() 均路由 assertEngineDeleteDispatch(pinned 17→19),os-dev 派发词/定义可加一行:测试假引擎的 delete() 必须路由 assertEngineDeleteDispatch —— 同一门禁一日两红(#5173、#5192) #5197 的门禁在正向工作。

    sys_file 元数据行从此不会再无声消失 —— 写失败即 500,运维能看见;这是 #5087「假成功」教训在存储层的收尾。


    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

Labels

bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions