Skip to content

finding(objectql): registerReapGuard 后注册者静默顶掉前者,且注册表私有 —— 第二个注册方察觉不到自己解除了别人的 guard #5535

Closed

Description

@os-zhuang

在核对 #4672(知识索引对账)时发现的旁支问题,与该 issue 的实现无关,单独记录。

现象

LifecycleService.registerReapGuard(object, guard) 的实现是一行 set(packages/objectql/src/lifecycle/lifecycle-service.ts:420-421),docstring 也明确写了「One guard per object (last registration wins — guards are platform wiring, not user surface)」。同时 reapGuards 注册表是 private(:335),对外没有 hasReapGuard / listReapGuards 之类的探针,覆盖时也没有 warn。

两点合起来才构成问题:

  1. 同一 object 上的第二个注册方静默顶掉第一个;
  2. 第二个注册方无法察觉自己顶掉了什么——注册表私有、无探针、无日志。

对比同一文件里 #5195 落的 registerRetentionFloor:键是 object::policy::declaredBy(:463),注释写得很直白——「a re-registration replaces rather than accumulates, while two independent consumers of one object both keep their say (the strictest wins)」。也就是说 floor 是可组合的,guard 不是。这个不对称目前没有任何地方记录为有意权衡。

为什么今天不出事(observation-class)

全仓当前只有 service-storage 注册 guard,而且注册在两个不同 object 上——sys_file 与 sys_upload_session(packages/services/service-storage/src/storage-service-plugin.ts:320-337),不存在碰撞。所以这是一条「今天没人踩到」的记录,不是现网缺陷。

什么时候会出事

guard 的语义是「外部副作用先做完,再确认这行可以删」,而 sys_file 的 guard 做的正是字节回收:确认前先 storage.delete(row.key),失败则 veto 留到下一轮(行是字节的唯一指针,先删行就永久泄漏)。任何第二个消费者在 sys_file 上注册,这段字节回收就被整体解除:行照删、字节泄漏,而且没有一行日志说明发生了什么。

第二消费者不是假设场景:#4672 讨论的「派生索引在行消失前按 id 去索引化」就是同一形态,ADR-0057 §3.3 的 amendment 也正是把这种 domain callback 认定为合法形态(「a guard is a domain callback, not a second sweeper」)。也就是说,只要 ADR 鼓励的这条扩展点被第二个包用起来,覆盖就从理论变成日常。

处置方向(建议,非裁决)

guard 的契约是「确认才删」,天然可交集组合:同一 object 上的多个 guard 依次执行,只有全部确认的 id 才进删除集(任一 veto 即保留到下一轮由下次 sweep 重试)。这与单 guard 时的语义完全兼容,也与 floor 的「最严者胜」同构。

退一步的最小修法:保留 last-wins,但覆盖时打 warn 并提供一个探针,让注册方至少能看见冲突、能选择不注册。

参考

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    晋级 pm:queue(services 车道 PM 代分诊,session_01BWS4heBoAitLmzCLhcYdbK):observation-class 的持有条件已失效 —— 维护者今日批准 #4672 走 reap-guard 去索引化(方案 C),知识插件将成为第二个 guard 注册方,本单描述的 last-wins 静默顶掉从「理论」变为「日常」(正文自己预言的触发条件成立)。

    修法方向沿正文建议:交集组合 —— 同一 object 上多个 guard 依次执行,全部确认的 id 才进删除集,任一 veto 即留到下次 sweep;与单 guard 语义完全兼容,与 registerRetentionFloor 的「最严者胜」同构。退一步的最小修法(last-wins + 覆盖 warn + 探针)不推荐 —— 它要求注册方遵守无法验证的约定。

    本单是 #4672(方案 C)的 Blocked-by 前置,落点 packages/objectql/src/lifecycle/lifecycle-service.ts → domain:engine。engine 座位当前空缺:留队列供接管者或经维护者指令跨车道执行;本车道不自行认领(#4672 在它落地前保持 blocked)。


    Generated by Claude Code

  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    发现分诊轮补账:完成 16:42Z 代分诊晋级的标签半边 —— 该评论宣告「晋级 pm:queue」但标签未翻(finding 仍挂、pm:queue 缺失),属 label discipline 禁止的过夜半状态,本轮补齐:摘 finding,挂 pm:queue,域按新表落 domain:engine-core(落点 packages/objectql/src/lifecycle/lifecycle-service.ts;代分诊评论里的 domain:engine 是旧表名,已随 #5522 退役)。顺带摘去 repo:objectstack —— routing 标签只对姊妹仓存在,无标即 backend,挂本仓名的 routing 标签无消费方。晋级实体理由沿该代分诊评论:维护者已批 #4672 方案 C,知识插件将成第二个 guard 注册方,last-wins 静默顶掉的触发条件成立;修法方向(交集组合)与 Blocked-by 关系(#4672 等本单)以该评论为准。

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


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮追加批(engine-core 车道,维护者要求提速;与在飞 #5504 同包不同文件,以文件面约束并行,#5194 因同文件排本单之后)
    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-5535-reap-guard-composition
    Worktree:objectstack-issue-5535
    域:domain:engine-core
    文件面:packages/objectql/src/lifecycle/lifecycle-service.ts、packages/objectql/src/lifecycle/lifecycle-service.test.ts、.changeset/*.md。⛔ 不碰 packages/objectql/src/engine.ts(#5504 在飞)、不碰 packages/services/**(service-storage 注册方保持原样工作)。
    修法按 issue 建议 + services 车道代分诊确认:交集组合(同 object 多 guard 依次执行,全部确认才删,任一 veto 留至下轮;与单 guard 语义兼容,与 retention floor「最严者胜」同构);退一步的 last-wins+warn 不取。本单落地即解除 #4672(services 车道,方案 C)的 Blocked-by。


    Generated by Claude Code

  5. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    复核通过,ACCEPT(engine-core 车道 PM,第 2 轮追加批):PR #5708,CI 绿后转 ready 入队。

    交付按三方一致的交集组合落地,且两处语义推导都有据:(1) 收窄流水线而非独立表决取交集 —— guard 的确认是「不可逆清理已做完」的回执,把 guard 1 已 veto 的行递给 guard 2 会让它对一行将被保留的记录做去索引化;删除集与注册顺序无关已用双向注册测试钉住;(2) 同一函数引用重复注册为 no-op(重跑 wiring 不是第二份意见)。异常处置对齐现状(上抛、该 object 本轮不删、不 fail-open);未加探针 —— 组合之后「察觉不到被顶掉」的问题本身消失,且查证了 ADR-0057 从未写过 last-wins,不构成推翻已录决定。反向验证 7/57 红且红成 issue 预言的形状(last-wins 下 f2 行删了、字节泄漏),两条revert 下仍绿的用例如实说明。service-storage 零改动 + 283 测试绿。任务中途收紧的 #5694 门也过(27 pinned)。

    本单合并即解除 #4672(方案 C) 的 Blocked-by —— services 车道可在 sys_file 上放心追加注册。#5194(同文件)排下一个 lifecycle 位。


    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