Repository navigation
Description
发现于 #5089(#5040 E2 端点匹配器)实现期,不在该单范围内,故单独立卡。
现象
packages/metadata/src/loaders/database-loader.ts 的三个读方法把任何存储异常吞成「没有」:
loadMany(type)(:740-753)——catch { return []; },连 warn 都没有;exists(type, name)(:765-773)——catch { return false; };stat(type, name)(:785-)—— 同形。
于是 sys_metadata 所在库不可达时,DatabaseLoader.loadMany('permission') 与「这个环境一条 permission 都没声明」返回完全一样的值。上层 MetadataManager.list() 也无从分辨:它拿到的是一次「成功的空读」,不是一个异常,所以连它自己的 logger.warn(...loader failed to loadMany...) 降级分支都不会触发 —— 整条链上没有任何一处会说出「读失败了」。
为什么这是缺陷而不是设计
ADR-0110 D3 已经为单数读路径立过这条规矩,理由写在 MetadataManager.loadDiagnosed 的注释里:
A miss and an outage are different facts with opposite security meanings, and plain
loadcannot express the difference: a loader that throws is warn-logged and skipped, so a database the metadata plane cannot reach returns the samenullas a name that was never declared. Callers that gate on a declaration MUST NOT read thatnullas "the author declared no gate" — an availability failure would silently widen access (the REST/actionsroute's fail-open branch, #3935).
loadDiagnosed 让单数读能分辨两者(degraded + errors)。复数读没有对应物,而且比单数读更糟:load 至少让异常冒到 manager 那层被 warn 记下,loadMany 在 loader 内部就把异常抹掉了,manager 那层的降级分支形同虚设。
凡是「按已声明清单做判断」的消费方都吃这一口 —— 权限/策略/共享规则清单读成空,按消费方的姿态不同,后果要么是静默 fail-open(放行),要么是静默 fail-closed(全站锁死);两种都在外部看起来「一切正常」。这正是 AGENTS.md 降级日志分级那节点名的形状:系统看着正常,而它声称掌握的东西其实没读到。
复现
const loader = new DatabaseLoader({ driver: brokenDriver, tableName: 'sys_metadata' });
await loader.loadMany('permission'); // => [] (驱动抛错,零日志)
await loader.exists('permission', 'admin_all'); // => false建议方向(裁决留给维护者)
- 复数读补齐
loadDiagnosed的对位物(loadManyDiagnosed→{ items, degraded, errors }),MetadataManager.list()至少把degraded记成 warn/error; - 或者让 loader 层照实抛出,由 manager 现有的
try/catch决定降级姿态 —— 这样至少logger.warn会真的响一次,消费方也能选择改用严格读。
两条都不是本 issue 要定的;要定的是「读失败必须能被说出来」这一点。
已知的一处下游影响(已在 #5089 里绕开,未修根因)
IMetadataService.matchEndpoint 的契约文本明确要求「读不到存储必须抛出,不得伪装成 miss(miss 会变成 404)」。#5089 为此没有走 list(),而是新增了一个私有的 listForIndex(),不包 try/catch,让 loader 的异常冒到调用方。这在 MemoryLoader / RemoteLoader 上是有效的,但对 DatabaseLoader 无效 —— 它在自己内部就吞了,谁也看不见。listForIndex() 的注释已经把这条限制写明并指向本 issue。
关联
ADR-0110 D3、#3935(fail-open 先例)、#5089(发现处)、AGENTS.md「Degradation log levels — warn vs error」。
Activity
os-zhuang commentedon Aug 4, 2026
认领:PM 循环第 1 轮
会话:
session_01Pbu27iNUfQCHeuS551Rqo7分支:
claude/issue-5108-database-loader-outage-signalWorktree:
objectstack-issue-5108域:
domain:engine文件面:
packages/metadata/src/loaders/database-loader.ts、packages/metadata/src/loaders/loader-interface.ts(如走 loadManyDiagnosed 方向)、packages/metadata/src/metadata-manager.ts(仅 list() 降级日志的最小触碰)、测试 + changeset(越界即停,报告说明)串行约束:⛔ 不碰
attachClusterPubSub/notifyWatchersLocal/listCache失效逻辑 —— 那是 #5109 的面,本单落地后下轮派。⛔packages/spec/**零改动(spec 车道另有会话);若结论是必须动IMetadataService契约才能修,停下回报 needs_decision。Generated by Claude Code
os-zhuang commentedon Aug 4, 2026
验收:ACCEPT —— PR #5183(等 CI 绿后入队)
会话:
session_01Pbu27iNUfQCHeuS551Rqo7(PM 循环第 1 轮)方向修正,公开记录:派发词里 PM 初读偏向方向 1(
loadManyDiagnosed),dev 读码后按证据选了方向 2(loader 按错误类型判决、照实抛出),PM 复核后认可 —— 三条论据都站得住:(a)schema-sync-errors.ts/#4825 已在同文件立过两次同形规矩,isMissingTableError判据现成;(b) #5089 的listForIndex()注释已把缺陷落点判给 loader;(c) 方向 1 无法独立满足验收 ——listForIndex()走loadMany,后者不抛则matchEndpoint在 outage 下照样 404,且可选方法会催生?.+??的消费端宽容(PD #12 禁区)。修正后的前提:issue 里「load至少让异常冒到 manager 被 warn」一句不成立 ——load()同样在 loader 内吞掉,ADR-0110 D3 的loadDiagnosed对DatabaseLoader此前恒degraded: false;五个读方法同形全修。落地内容:唯一良性例外 =
sys_metadata未 provisioned(答空、不缓存);其余一律原样抛出。MetadataManager.list()降级分支第一次真的生效且升到error(每次故障说一次、恢复 un-say);loadDiagnosed真报degraded;matchEndpoint对破损 DatabaseLoader rejects 而非 404 形undefined。行为变化(exists()/listNames()故障改抛出)已核无生产调用方。声明外触碰一处,接受:manager 的
loadMany()同缝同判决(独立 commit)—— 同一故障在同文件报两个级别是错的那个被抄走的方式,理由成立;未越入 #5109 的面(attachClusterPubSub/listCache失效逻辑零改动,已对 diff 核验)。实况核验:draft ✓ / Fixes 行 ✓ / 4 文件在面内 ✓ / spec 零改动 ✓ / metadata 403 + metadata-protocol 339 + runtime 1270 + cli 612 全绿,durability-log-level 门禁 12 缝全响 ✓ / 两个方向都钉(outage 响亮、首启不炸、且被区分)。
→ #5109(集群失效)下轮即派,基于本单合并后的 main。
Generated by Claude Code
feat(metadata): endpoint matcher — matchEndpoint lazy index (objectst…
fix(metadata): DatabaseLoader 的读故障不再被吞成「什么都没声明」 (objectstack-ai#5108) (…
fix(metadata): 降级的 list() 结果按「降级」缓存 —— degraded 标记 + 2s 短 TTL (object…
fix(metadata-protocol): an unreadable sys_metadata is an outage, not …
docs(metadata): re-anchor the dead tracker citations to the commits t…