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. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  2. 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

  3. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    and removed
    pm:queueReady for the PM dispatch loop
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  4. 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

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 6, 2026
  6. self-assigned this
    on Aug 6, 2026
  7. 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

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

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