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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions