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 行里声明的 _locksys_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

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