Repository navigation
finding(objectql): reap 的每轮上限是「按 (对象, where 作用域)」计的 —— N 个租户覆写把一轮的实际预算变成 (N+1) × 10000 行 #5756
Description
Activity
分诊(发现分诊轮,#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,
lifecyclesettings 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
- 域:
Findings triage (#4949 discipline): hold —
findingretained,domain:engine-coreunchanged.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) andREAP_MAX_BATCHES_PER_SWEEP = 20(:325) are unchanged; the per-call budget loop is stillfor (let batch = 0; batch < REAP_MAX_BATCHES_PER_SWEEP; batch++)at:1209insidebatchedReap(), so it is still counted per invocation whilereap()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–:31and repeated at:1177/:1187–:1188. The only commit touchingpackages/objectql/src/lifecycle/lifecycle-service.tssince the last grading isdb0d53c(#5747 / PR #5999, truthy-testing the scalarwhere.idin 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:
- Unreachable today. The trigger is tenant-level retention overrides (P4 governance,
lifecyclesettings 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) × 10000inflates one sweep's total work, not any one statement. - 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
- Unreachable today. The trigger is tenant-level retention overrides (P4 governance,
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsFindings 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-servicetouch. Veto window applies.
Generated by Claude Code
观察类 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);$or: [{organization_id: {$nin: [...]}}, {organization_id: null}])。于是 N 个租户覆写 ⇒ 一个对象一轮最多删 (N+1) × 10000 行,而不是注释承诺的 10000。
为什么这不是 #5194 的回归,也不是当下的故障
lifecyclesettings namespace),目前没有部署在用;值得记下来的原因
「bound one sweep's work」是文件里写明的承诺,而在租户覆写存在时它字面上不成立 —— 下一个读这段注释的人会按 10000 估算一轮的成本。要么把预算改成跨 pass 共享(一个对象一轮一份 10000,按 pass 依次消耗),要么把注释改成「per (object, scope)」并说明为什么这样是对的。两种都是小改动,但属于语义取舍,不该由本 PR 顺手决定。
PR #5753 的正文里已如实记了这一条。
Found-during: #5194 / PR #5753