Skip to content

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

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

  2. self-assigned this
    on Aug 6, 2026
  3. 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

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions