Skip to content

finding(objectql): LifecycleService 的无 guard reap 每次 sweep 发一条无上限 DELETE —— 首次给存量大表加 retention 时是一条长事务 #5194

Closed

Description

@os-zhuang

观察类 finding,来自 #5179 的实现复核(PM 在派发里专门问过「清理批量要有上限」)。今天没有用户会撞到,但记录在案由 PM 定级。

事实(origin/main)

  • LifecycleService.reap()(packages/objectql/src/lifecycle/lifecycle-service.ts:777-783)在没有注册 reap guard 的对象上,一次 sweep 发的是单条 engine.delete(object, { where: { created_at: { $lt: cutoff }, ...onlyWhen }, multi: true }) —— 匹配多少行删多少行,没有 limit、没有分批;
  • 分批只存在于两条旁路:guardedReap(REAP_GUARD_BATCH_SIZE = 500 × REAP_GUARD_MAX_BATCHES_PER_SWEEP = 20,:924)和 Archiver(ARCHIVE_BATCH_SIZE = 500 × 20)。二者的注释都写明分批的理由是「bound one sweep's work, drain the backlog across sweeps」—— 同一个理由对无 guard 的 reap 同样成立,只是没落实;
  • 后果场景:一张已经积了大量行的表第一次被加上 retention(正是 service-queue: completed 任务行无人清理 —— purge() 零生产调用方、sys_job_queue 未声明 retention,队列表只增不减 #5179 对 sys_job_queue 做的事,也是任何新声明 lifecycle 的表的必经一次),那次 sweep 会发一条扫遍历史行的 DELETE。SQLite 上是一条长写事务(单连接池期间其它写者等锁),Postgres 上是一次大 autovacuum 债务。
  • 稳态下没有问题:每小时一扫,增量很小。

为什么单独记而不是在 #5179 里改

这是 LifecycleService 对所有 lifecycle 表的共性行为(sys_activity 14d、sys_job_run 30d 一直如此),不是队列表特有;在 #5179 的文件面(packages/services/service-queue)里也够不着。若要修,合适的形状是给无 guard 的 reap 也套上 BATCH × MAX_BATCHES_PER_SWEEP 的既有姿态(需要 driver 侧支持带 limit 的 delete,或退化为「读 id → 按 id 删」),代价是要在两种删除路径之间做取舍 —— 值得单独定夺,别当成 #5179 的搭车。

Found-during: #5179 / PR #5192

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 4, 2026
  2. added
    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
  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    发现分诊轮判级(session_01VkPSGsX9o17MsGv3Lbxu2w,2026-08-05):晋级 pm:queue,补 domain:engine(落点 packages/objectql/src/lifecycle/lifecycle-service.ts)。前提核验见 #4604 01:57Z(对 b4ad98435 成立)。理由:存量大表首次加 retention 必经一条无上限 DELETE(SQLite 长写事务锁全库)—— 用户可达;修法姿态既有(guardedReap/Archiver 的 500×20 分批),把同一理由落实到无 guard 路径即可。维护者可否决。


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 3 轮(engine-core 车道)
    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-5194-unguarded-reap-batching
    Worktree:objectstack-issue-5194
    域:domain:engine-core
    文件面:packages/objectql/src/lifecycle/lifecycle-service.ts、lifecycle-service.test.ts、.changeset/*.md。⛔ 不碰 packages/plugins/driver-* / packages/drivers/**(不给 driver 契约加带 limit 的 delete —— memory/mongo 族在 #5499 冻结中)、不碰 engine.ts(#5480 同批在飞)。

    机制裁决(PM 拍板,维护者可否决 —— 属实现取舍,issue 已给两选项):采「读 id → 按 id 删」的既有分批姿态(500 × 20/sweep,余量跨 sweep 排空),⛔ 不改 driver 契约。提示:#5708 刚把 guardedReap 改成交集组合 —— 无 guard 对象等价于「零 guard 的交集 = 全确认」,评估直接统一走 guardedReap 机制是否成立(零 guard 时无额外副作用、批量语义相同);若统一可行则优先(一条删除路径好过两条),不可行则平行套用同款分批,报告里说明取舍。


    Generated by Claude Code

  6. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    PM 评审(会话 session_01V7WetGmnfoXNn8cLieKKmx):PR #5753 ACCEPT,待 CI 绿后放行。

    已读全量 diff 核对,要点:

    1. 统一路径成立:guardedReap → batchedReap,零 guard 即空交集(全确认),dispatch 的判据从「有无 guard」正确换成「能否读行」(canReadRows)。回退肢是「无 find 路径」而非「无 guard 路径」,且 guard+无 find 的 fail-safe skip 保留在其前——三种形态各得其所。
    2. 三处必须 settle 的点都有测试钉住:无 id 行在 guard 前丢弃(避免 where:{id:undefined}+multi:true 被 dispatch 路由到 deleteMany 跑整批 cutoff 谓词——这是真实的误删面,单独成例);每页查 每个 os migrate 子命令关停时,悬空引用巡检都会把 sys_metadata / sys_view_definition 报成 unreadableObjects(连接已关闭) #4747 abort 位(stop() 中途止页,在飞页完成、不再读后 19 页);无 find 引擎保留原 bulk DELETE(retention 不静默失效)。
    3. fixture 分诊合格:整例替换的 regression pin 钉的正是被移除的肢,保留会假绿;替换后的断言(他对象 guard not.toHaveBeenCalled + 逐 id 删)比原例更尖锐。三个租户 governance/floor 用例只是谓词断言面从 deletes[].where 移到 finds[].where,被测性质未变且补了逐 id 删断言。
    4. 诚实记录到位:文件头「每对象每轮一次 afterDelete」自 finding(objectql): registerReapGuard 后注册者静默顶掉前者,且注册表私有 —— 第二个注册方察觉不到自己解除了别人的 guard #5535 起已失真,本次一并改正并写明按行触发的代价与今日对象群为何零代价(SKIP_OBJECTS 逐一核对);未加 fields:['id'] 投影的理由(无实测支撑的优化不建,读形状统一优先)符合裁决精神。
    5. 边界遵守:未碰 engine.ts / driver 包 / spec;changeset patch;消费半径(service-queue / dogfood)已跑;反向验证 9 例转红、no-find 例不转红的方向判断正确。

    一处知悉即可、不要求改:total += confirmed.length 计的是已发出的逐 id 删除数,并发竞争下与 driver 实删数可能有 ±,这是 guardedReap 原有语义,本 PR 未劣化。

    CI 绿后我会转 ready 并挂 auto-merge。


    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