Skip to content

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

Closed

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. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 6, 2026
  3. self-assigned this
    on Aug 6, 2026
  4. 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

  5. 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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions