tracker: full-history PR walks (reaper done-signal, labctl pr list) grow unboundedly and hammer the forge #176
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#176
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
Everything that touches the forge gets slower as the repo accumulates PRs, forever. Measured live:
labctl pr liston this repo (~174 PRs) takes 6.7 seconds. The forge (separate host, git.cloonar.com) absorbs a continuous stream of its most expensive query, which also slows issue pages the UI proxies, git fetches, and land-pr flows — invisible in the lab server's own htop.Mechanism (code-confirmed, measured)
forgejo.Client.Pulls(internal/tracker/forgejo/forgejo.go:246) lists withstate=alland paginates until an empty page, 50 per page, sequentially — i.e. every PR ever created. Forgejo's pulls endpoint costs ~1.3s/page here, so the walk is ~1.3s x (N/50 + 1): linear growth, +1.3s per 50 PRs merged. At the current pace (~170 PRs in 10 days) it doubles roughly every two weeks; the empty-page termination adds one guaranteed wasted request per walk.Call sites that run the walk:
internal/afk/reaper.go:92): every tick (default 30s) per repo with active AFK runs, to answer the done-signal question "does an open-or-merged PR exist with head = this run's branch". 854 walks in 17h at diagnosis time. As the walk grows past the tick interval, done-signal detection lags and ticks coalesce.labctl pr list(agent surface): used by agents and /land-pr, so agent sessions stall on it too.Fix direction
GET /repos/{owner}/{repo}/pulls/{base}/{head}(and head filters on the list endpoint). The reaper needs one bounded request per run, not the repo's PR history. Preserve the exact done-signal semantics: open or merged counts, closed-unmerged does not (tracker.DonePull).labctl pr list: bound the walk — open PRs plus a recent-merged window (e.g. sort by recency, stop after a page horizon), or make the state filter explicit and default it to open.Issues(state)walk (state=all/closedviews) for the same shape while in there; the open-issue list is currently one page and fine.Constraints: the tracker seam is provider-neutral (forgejo/github/builtin all implement it) — the per-branch query needs a seam method (e.g.
PullForHead(branch)) with a conformance test, not a forgejo special case. Watch the GitHub client for head-filter syntax differences (owner:branch).Verification
time labctl pr listbefore/after (6.7s -> sub-second). Reaper: tracker metricslab_tracker_requests_total{op="pulls"}should stop growing per tick; done-signal latency for a fresh AFK PR stays within one tick.Agent Brief
Category: bug
Summary: Replace unbounded full-history PR walks (reaper done-signal,
labctl pr list) with bounded per-branch / windowed queries behind the tracker seam.Current behavior:
The
Trackerseam'sPulls()lists every PR the repo has ever had (state=all, sequential 50-per-page pagination until an empty page — so one guaranteed wasted request per walk). Two hot paths run it:tracker.DonePull, matched client-side).labctl pr list(the agent surface behindGET /agent/v1/prs) runs the same walk, so agent sessions and /land-pr flows stall on it.Cost is linear in repo history and grows forever: at triage time the repo has 80 PRs → 3 sequential requests → ~1.7s on an unloaded forge, measured at 6.7s during the perf incident when the reaper's own request stream had loaded the forge (the walk makes the forge slow, which makes the walk slow). Growth is one extra page per 50 merged PRs, and this repo merges ~50/week. The Forgejo and GitHub clients share the identical shape; the builtin tracker is a cheap store query and is not a problem.
Desired behavior:
GET /repos/{owner}/{repo}/pulls/{base}/{head}and head filters on the list endpoint; GitHub's list endpoint takeshead=owner:branch(note the owner prefix — syntax differs from Forgejo).tracker.DonePullcontract, including its collision preference when several PRs share a head).labctl pr list: the request count is bounded and independent of total history. Default view: all open PRs plus a recent window of merged/closed ones (recency-sorted with a fixed page horizon is fine). The walk-to-empty-page pattern disappears from this path.Key interfaces:
Trackerinterface — add a seam method (e.g.PullForHead(ctx, headBranch)returning aPullRefor its absence) implemented by all three backends (forgejo, github, builtin), not a forgejo special case. Update thePulls()doc contract if its "state=all is required for the done-signal" rationale moves to the new method.tracker.DonePull/PRPresent— the pure projection functions; keep their semantics, adapt their input if the reaper no longer holds a full listing.lab_tracker_requests_totaltracks it; the reaper's per-tickop="pulls"count should stop growing with history (or disappear in favor of the new op).pr listvariant needs a page-horizon mode rather than walk-until-empty.Branch-deletion caveat (pin in a test): merged PRs whose head branch was since deleted come back from Forgejo with a synthetic head ref (
refs/pull/N/head), not the branch name. The done-signal is safe because the reaper only queries branches of active runs (the branch still exists until after the run is reaped), but the per-head query must be specced/tested against live branches only — don't assume a merged-and-deleted branch remains findable by name.Acceptance criteria:
time labctl pr liston this repo drops from multi-second to sub-second, and its forge request count no longer grows with merged-PR history.pr liststill shows all open PRs; recently merged PRs appear in the default view (bounded window documented in the command output or docs).Issues(state)walk is checked for the same unbounded shape; fixed the same way ifstate=all/closedviews walk full history, otherwise explicitly noted as fine.Out of scope:
GET /instancescommits_behindperf (#152).DonePull's collision/state semantics.