Skip to content

getEffectiveLock 的 overlay 读用裸 catch,sys_metadata 读失败时保护闸门 fail-open(_lock 落成 'none',写/删被放行) #5706

Description

@os-zhuang

Blocked-by: #5532(同文件同族在飞:PR #5705 引入本单建议复用的 rethrowUnlessMetadataStoreUnprovisioned;其落地后 sweep 回队)。

做 #5532(getMetaItem overlay 读把 outage 吞成 miss)时,在同一个文件里发现的同族点。不在那单范围内(#5532 / PR #5705 的文件面被限定为 getMetaItems / getMetaItem 的四处 overlay 读 + getMetaItemCached 终末错误),按 Prime Directive #10 单独记在这里,unassigned。

与 #5532 的关系:同一条裸 catch 家族、同一个文件,但消费方不同、后果类别不同 —— #5532 的后果是「读」被讲错(可用性故障被讲成不存在),这条的后果是「保护闸门失效」,写路径被放行。

位置

packages/metadata-protocol/src/protocol.ts,getEffectiveLock(私有,以内容定位:注释 // 2. Overlay row. 之后的 try):

        } catch {
            // DB unavailable — fall through to 'none'.
        }
        return { lock: 'none', lockReason: undefined, lockSource: undefined };

为什么是缺陷

getEffectiveLock 是 ADR-0010 §3.3 锁闸门的唯一判据来源,两个调用点都是写路径的准入:

  • assertLockAllowsWrite(save / publish / rollback)
  • assertLockAllowsDelete

两者都是 const state = await this.getEffectiveLock(...) 然后 evaluateLockForWrite(state.lock)。lock: 'none' 意味着「没锁」→ refusal 为 null → 返回 null,写放行。

于是:overlay 行里声明的 _lock 在 sys_metadata 读失败时静默变成「没有锁」,一次被拒绝的写会变成一次被允许的写。这是 ADR-0049 的 fail-closed 方向反过来 —— 一个「读不到」被当成「作者没声明保护」,正是 ADR-0110 D3 点名的那个禁止推论,只是这一次落在安全判定上而不是展示上。

审计侧也一并失真:放行路径不会写 outcome: 'denied' 的审计行,所以事后也看不出这次写本该被拒。

缓解与真实窗口(如实记录,不夸大)

  1. artifact 级锁不受影响:getEffectiveLock 先查 lookupArtifactItem(纯内存 registry),打包件声明的 _lock 仍然生效。失效的只有 overlay 来源 的锁(lockSource: 'overlay')。
  2. 存储整体不可用时,写本身通常也会失败,所以窗口不是「元数据库全挂」,而是读失败但写成功的场景:瞬时错误、单条查询超时、只读副本故障、读写分离下的读侧异常、连接池局部耗尽。
  3. this.environmentId === undefined(control-plane)时两个 assert 直接 return null,不走这里。

即便如此,「保护闸门在读失败时默认放行」本身就是不该存在的形状,不依赖窗口大小。

建议修法(与 #5532 / PR #5705 对齐,不代裁决)

PR #5705 已经在同文件引入了 rethrowUnlessMetadataStoreUnprovisioned:按错误类型判别,isMissingTableError(表还没建 → 确实没有 overlay 行 → 'none' 是真相)良性放行,其余抛 status: 503 / code: SERVICE_UNAVAILABLE,驱动错误挂 cause。这里直接复用同一个私有方法即可,一处 catch 的改动。

需要注意的差异:这条路径的抛出会让 saveMetaItem / deleteMetaItem 在元数据库读故障时以 503 失败,而不是「当作没锁然后去写」。这是期望的方向(拒绝一次不确定的写,好过放行一次本该被拒的写),但属于 wire 可见变化,应在 changeset 写明。

未验证:是否还有其它调用点通过别的路径读同一行锁状态(resolveLockState 走的是已取到的 item,不经过这里)。

关联

#5532 / PR #5705(同文件同家族,读侧)、ADR-0010 §3.3(锁语义)、ADR-0049(declare-and-enforce / fail-closed)、ADR-0110 D3(miss ≠ outage)、#5108(DatabaseLoader 复数读,先例)。


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊:入队 pm:queue + 挂 pm:blocked(Blocked-by: #5532 已补正文),域 domain:engine-core。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    解锁 + 认领:PM 循环第 3 轮(engine-core 车道)。Blocker #5532 已随 PR #5705 合并关闭,按 unlock sweep 摘 pm:blocked 回队并立即认领(fail-open 保护闸门优先占 protocol.ts 空位,#5264 排下一位)。

    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-5706-lock-gate-fail-closed
    Worktree:objectstack-issue-5706
    域:domain:engine-core
    文件面:packages/metadata-protocol/src/protocol.ts(getEffectiveLock 的 overlay 读一处 catch)、metadata-protocol 测试、.changeset/*.md(写明 wire 可见变化:save/publish/rollback/delete 在元数据存储读故障时 503,而非当作没锁放行)。
    修法按 issue 建议:复用 #5705 刚落的 rethrowUnlessMetadataStoreUnprovisioned(未建表良性 → 'none' 是真相;其余照实抛 503)。「拒绝一次不确定的写,好过放行一次本该被拒的写」。


    Generated by Claude Code

  4. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    复核通过,ACCEPT(engine-core 车道 PM,第 3 轮):PR #5736,CI 绿后转 ready 入队。

    交付精确到裁定范围:一处 catch + JSDoc,复用 #5705 的判别,闸门判定逻辑零改动(503 自然上抛有测试钉住);窗口建模修正到位(「读失败但写成功」—— dev 的第一版 harness 因全挂掩盖闸门,自行发现并修正);修复前 fail-open 实测复现(success:true 且写真执行、审计只有 allowed 行 —— 缺陷长期不可见的原因被一并解释);反向验证 5 红且如实说明与预测 4 的偏差,7 绿各有承重理由(含「真 miss 照常放行」防「什么都拒假装 fail-closed」)。issue 未验证点全枚举:getMetaItem 路径不同病(已被 #5705 覆盖),layered 路径同形但归 #5707 持有(含其未定设计选择),零重复立单。wire 变化(读故障时写路径 503)已进 changeset。

    本单从 #5705 合并解锁到交付共 ~35 分钟,是本车道当日最快闭环。#5264 排 protocol.ts 下一位。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions