Skip to content

finding(objectql): reap 的每轮上限是「按 (对象, where 作用域)」计的 —— N 个租户覆写把一轮的实际预算变成 (N+1) × 10000 行 #5756

Description

@os-zhuang

观察类 finding,来自 #5194 / PR #5753 的实现。今天没有用户会撞到,记录在案由 PM 定级。

事实(origin/main + PR #5753)

REAP_BATCH_SIZE (500) × REAP_MAX_BATCHES_PER_SWEEP (20) 的注释说的是「bound one sweep's work」。实际上这个预算是在 batchedReap() 一次调用内计的,而 reap() 在有租户覆写时会多次调用它(lifecycle-service.ts:1128 起):

  • 每个声明了 maxAge/expireAfter 覆写的租户一个 pass(organization_id: t.tenantId + 该租户自己的 cutoff);
  • 外加一个全局 pass($or: [{organization_id: {$nin: [...]}}, {organization_id: null}])。

于是 N 个租户覆写 ⇒ 一个对象一轮最多删 (N+1) × 10000 行,而不是注释承诺的 10000。

为什么这不是 #5194 的回归,也不是当下的故障

值得记下来的原因

「bound one sweep's work」是文件里写明的承诺,而在租户覆写存在时它字面上不成立 —— 下一个读这段注释的人会按 10000 估算一轮的成本。要么把预算改成跨 pass 共享(一个对象一轮一份 10000,按 pass 依次消耗),要么把注释改成「per (object, scope)」并说明为什么这样是对的。两种都是小改动,但属于语义取舍,不该由本 PR 顺手决定。

PR #5753 的正文里已如实记了这一条。

Found-during: #5194 / PR #5753

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊(发现分诊轮,#4949 纪律):持有(留 finding)。

    • 域:domain:engine-core(维持)。落点:packages/objectql/src/lifecycle/lifecycle-service.ts 的 reap() / batchedReap() 预算计法(REAP_BATCH_SIZE × REAP_MAX_BATCHES_PER_SWEEP 在每次 batchedReap() 调用内计,而租户覆写会让 reap() 多次调用它)。
    • 为什么还留着:① 今日不可达 —— 触发前提是租户级 retention 覆写(P4 governance,lifecycle settings namespace),目前无部署在用;② 真正要定的是语义取舍,不是缺陷修复:预算改成跨 pass 共享一份 10000,还是把注释改成 per (对象, 作用域) 并说明为什么这样是对的。两条都是小改动但结论相反,不宜由路过的 PR 顺手定。
    • 过时前提检查:origin/main f205c32 上 ARCHIVE_MAX_BATCHES_PER_SWEEP(:314)/ REAP_MAX_BATCHES_PER_SWEEP(:325)与 batchedReap(:1189 起)均与正文一致;PR fix(objectql): bound every lifecycle reap, not just the guarded ones (#5194) #5753 已合入,未改预算计法(正文自陈,已核)。
    • 重启条件:(a) 租户级 retention 覆写进入实际部署或进入验收面;或 (b) 有人为别的原因动这段预算 / 注释时顺路一并定口径 —— 届时晋级 pm:queue。
    • 同文件提醒:finding(objectql): Archiver 的 20 批循环不看 #4747 的 abort 位 —— teardown 期间仍会继续读写正在关闭的 datasource #5755(本轮已晋级)落在同一文件的 archive 循环上,是一行 abort 检查,与本条的取舍正交;两单不要合并处理。

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


    Generated by Claude Code

  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage (#4949 discipline): hold — finding retained, domain:engine-core unchanged.

    Stale-premise check (origin/main @ ee3bde1, read from the tree rather than from the issue): the finding is intact line-for-line. REAP_BATCH_SIZE = 500 (:324) and REAP_MAX_BATCHES_PER_SWEEP = 20 (:325) are unchanged; the per-call budget loop is still for (let batch = 0; batch < REAP_MAX_BATCHES_PER_SWEEP; batch++) at :1209 inside batchedReap(), so it is still counted per invocation while reap() still invokes it once per tenant override plus once globally; and the promise the finding objects to is still in the file header at :30–:31 and repeated at :1177 / :1187–:1188. The only commit touching packages/objectql/src/lifecycle/lifecycle-service.ts since the last grading is db0d53c (#5747 / PR #5999, truthy-testing the scalar where.id in the delete dispatch) — a different code path that does not touch the budget arithmetic.

    Why it still holds — both original reasons survive re-checking, and neither has an expiry:

    1. Unreachable today. The trigger is tenant-level retention overrides (P4 governance, lifecycle settings namespace); still nothing deployed on it, and the finding's own analysis stands that the long-transaction hazard did not return — deletes remain single-row by id, so (N+1) × 10000 inflates one sweep's total work, not any one statement.
    2. The open question is a semantic choice, not a defect. Share one 10,000-row budget across passes, or keep per-scope budgets and correct the comment to say "per (object, scope)" and why that is right. Both are small; they point opposite ways; a passing PR should not settle it by accident.

    Restart condition (unchanged, both still unfired — restated so the next round does not have to re-derive them): (a) tenant-level retention overrides reach a real deployment or an acceptance surface; or (b) someone edits this budget or its comment for an unrelated reason, at which point the wording gets settled in that PR and this promotes to pm:queue.

    Same-file note, refreshed: #5755 — the sibling flagged last round as an orthogonal one-line abort check in the archive loop — is closed/completed, so it is no longer a serialization consideration for this file.

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


    Generated by Claude Code

  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): closing, not planned. Per-tenant reap passes are the guard path's designed behavior; per-pass batching keeps statements single-row, so the only gap is a comment promising a tighter budget than the code keeps — that one-line correction rides the next lifecycle-service touch. Veto window applies.


    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

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions