Forgejo package registry has no retention rules: apply the Stage 1 cleanup spec (disk pressure since relieved) #903
Labels
No labels
burndown-2026-06
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/infrastructure#903
Loading…
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?
Measured 2026-08-23 ~00:20Z, ops seat, read-only throughout. Nothing was changed. Classification only, per the disk-pressure runbook's contract.
Headline
kai-serverroot is at 90.75%, statuscritical, 44 GiB available, 27.6 GiB past the critical threshold.The dominant consumer is the Forgejo package registry at 93 GB, and it has no cleanup rules configured at all. That is not a tuning problem, it is an absent policy:
Attribution, top down
Every figure below is a complete scan, not a lower bound, unless marked.
Within the 159.9 GiB of PVCs, one claim is 81% of it:
And inside
forgejo-data(139.7 GiB as Forgejo sees it):So Forgejo application data is ~133 GiB, about 28% of the entire disk, and two directories account for all of it. Git objects are a rounding error at 1.3 GiB, and PostgreSQL is healthy at 604 MB.
Why the registry cannot self-heal
Zero unreferenced blobs. There is nothing for garbage collection to reclaim. Every byte is held by a live package version, so the only lever is retention, and retention is unset.
Version counts by package:
2,039 package versions retained, and
older_than_30dis 0 for every owner. Oldest version is 2026-07-24. So this entire 93 GB accumulated in about 30 days, which is roughly 3 GB/day of permanent growth, and none of it has aged out because nothing ages it out.agentic-osalone is 695 versions and 59 GB. That is the dev-base image, which publishes five language payloads across two architectures per release.This reframes agentic-os#1060
coilyco-flight-deck/agentic-os#1060reports the dev-base release blocked becausepublish-lang-rustbuilds but cannot push, with a genericunknownerror, at 87% disk. It reasoned circumstantially toward "out of room" and correctly declined to claim it.The measurement supports that reading and adds the mechanism: the registry is full of 695 prior versions of the very image whose push is failing. The publish pipeline fills the disk that the next publish needs. It is a self-reinforcing loop, not a registry fault, and no amount of retrying
publish-lang-rustwill clear it.Tonight's step
The 7-day trend is a sawtooth oscillating 69% to 84%, reset daily by the runner recycle, with the floor creeping up (70%, then 71%, then 72%). Then from roughly 21:00Z it stepped 72% to 90.75% and plateaued.
That correlates with active
agentic-ospublishing: a package version created 23:33:49Z, a successfulreleaserun at 00:18Z, and further runs at 00:15 to 00:17Z. Correlation with timestamps, not a proven causal chain. Stated as the leading hypothesis.Remediation plan
Stage 1: stop the growth (config only, no data deleted)
Configure Forgejo package cleanup rules per owner and type. This is a supported built-in feature that is currently entirely unconfigured, and it is by far the highest-leverage change. It needs one decision: how many versions to keep per container package, plus whether to express it as
keep_count,remove_days, or both.A
keep_countin the 10 to 20 range on container packages would cutagentic-osfrom 695 versions to a small fraction while keeping every recent rollback target. Immutable-SHA deploy tags mean old versions are only needed as far back as you would realistically roll back.Stage 2: reclaim (attended, needs a rollback artifact)
Once rules exist, run the managed cleanup then registry GC. Per
references/forgejo-storage.mdthis requires a fresh restic recovery artifact first, since packages are application data under the backup contract. Expected reclaim is most of the 93 GB.Stage 3: attachments, separately
data/attachmentsis 39.4 GiB and is not covered by package retention. Nothing here establishes what is writing it. That wants its own measurement before any policy.Stage 4: structural
forgejo-datarequests20Giand holds 139.7 GiB. local-path enforces no quota, so the claim size is documentation rather than a limit. Either move to a quota-enforcing class or monitor the claim directly.kai-disk-pressure-watchpolls from ser8 every five minutes and alerts at 85%, deliberately read-only. The only automated reclaim on the box is the daily runner recycle, which frees DinD scratch and nothing else. Stage 1 may make that acceptable, and that is worth deciding explicitly rather than by default.k3s/server/dbis 8.5 GiB, large for a single-node kine datastore. Cluster events live there and this estate has had sustained event storms. Inference, not established.Not established
/homeat 85.9 GiB is unexamined./var/lib/rancher/k3s/agent/containerdis ~55 GiB by subtraction. The direct scan timed out, so that figure is arithmetic rather than measurement.get_deleted_open_filesreturned 360 permission errors and timed out, so deleted-but-open files are unmeasured rather than ruled out.Related
coilyco-flight-deck/agentic-os#1060- the release blocked by this, reframed abovecoilyco-flight-deck/infrastructure#901- three stuck local-path PVs, ~3 GiB, separate mechanismcoilyco-bridge/deploy#677- closed today, reclaimed 5 GiBStage 1 spec: Forgejo package cleanup rules
Grounded in the running version (
data.forgejo.org/forgejo/forgejo:16.0.2-rootless), the upstream source, and this instance's own swagger. Config only. Deletes nothing until Stage 2.What Forgejo actually offers
One rule is a
PackageCleanupRulerow with seven configurable fields:Enabled- on/offType- the package type the rule governs (container,generic, ...)KeepCount- how many versions to always keep, per packageKeepPattern- regex, matches are never deletedRemoveDays- only versions older than N days are eligibleRemovePattern- regex, only matches are eligibleMatchFullName- patterns matchpackage/versionrather than bareversionScope, and why it is workable here
The model carries
UNIQUE(s)acrossOwnerIDandType, so there is exactly one rule per owner per package type. Rules cannot be written per package.KeepCountapplies per package inside the rule, which is what makes a single rule per owner acceptable. One rule oncoilyco-flight-deck/containertrimsagentic-osfrom 695 versions while leavingbluesky-mcpat 11 andquireat 1 untouched.Two semantics that determine the config
KeepCountalone is sufficient.services/packages/cleanup/cleanup.goruns protections in order and deletes when none fire:sha256:), handled by mass-digest cleanupKeepCountKeepPatternmatchesRemoveDaysRemovePatternis set and does not matchWith
RemovePatternempty andRemoveDayszero, checks 4 and 5 never protect anything. They are narrowing filters, not prerequisites.RemoveDaysis a no-op on this data. The measurement above showsolder_than_30d = 0for every owner, oldest version 2026-07-24. Any age-based rule deletes zero bytes. This has to beKeepCount.The rules
Four rules,
KeepCount = 15, every other field left empty.Projected effect on version counts, derived from the per-package table above:
coilyco-flight-deck- 896 to ~137. agentic-os 695→15, mcp-beaver 47→15, agent-proxy 38→15, ward 33→15, ward-mcp 18→15, code-fleet-watch 18→15. Untouched: node-stats-mcp 15, bluesky-mcp 11, reddit-mcp 10, lunch-money-mcp 10, quire 1coilyco-gamingcontainer - 621 to ~86. sirens-echo 469→15, eco-app 51→15, galaxy-gen 36→15, factory-game-v3 33→15, steam-mcp 21→15. Untouched: sirens-discord-ops 11coilyco-bridge- 138 to ~32. website 121→15. Untouched: reference-media 12, atlas 4, playwright-mcp 1coilyco-gaminggeneric - 384 versions, only 46 MB total. Included for hygiene, not for bytesRoughly 1,400 of 2,039 versions removed. No byte estimate is given deliberately: container layers are shared across versions, so reclaim is not linear in version count. The preview produces the real number.
Why 15 and not lower: deploys pin immutable source SHAs, so old versions matter only as far back as a realistic rollback. Fifteen covers well over a week for every package except the two highest-churn ones, and the number is trivially adjustable before anything is applied.
Sequencing, preview first
Risk to weigh before enabling
Forgejo #9358 reports container cleanup pinning one core at 100% for about 50 minutes, with pages timing out and database queries taking 40 seconds, on an instance holding 3,178 artifacts / 42 GiB.
This registry is 9,633 blobs / 93 GB, roughly double that. Two things to check first:
sha256:digest versions and defers them to mass-digest cleanup (#10297, backported to v13.0). This instance runs 16.0.2, so it should be included. That is inference from version ordering and has not been verified against the running binary.forgejopod already shows 21 restarts, most recent 9h ago. A CPU-heavy cleanup pass is exactly the load that would worsen that, and preview runs the same query path as the rule.Preferably run the first enabled pass at a quiet hour with the pod under observation.
These rules cannot be tracked in git, and that is a real gap
Checked this instance's own
swagger.v1.json. There is no cleanup-rule endpoint. The complete package API surface is:So cleanup rules are web UI only, persisted in the
package_cleanup_ruletable with no tracked source. That conflicts with this estate's config-placement rule, and it means the retention policy becomes hand-set state that nothing converges or validates.Two options, and I recommend the first:
doctor-style drift check that reads the table and compares against a tracked expectation.DELETE /packages/{owner}/{type}/{name}/{version}. Tracked in git, but it reimplements a feature Forgejo already has, deletes version by version, and would likely hit exactly the performance profile #9358 describes.Verification after enabling
aosguard ops forgejo-storage measurereruns cleanly and showspackageswell below 93.3 GiBunreferenced_blobsbecomes non-zero between the rule pass and blob GC, then returns to 0get_filesystem_pressurereturnsstatusbelowcriticalforgejopod restart count does not increase across the passOut of scope for Stage 1
data/attachmentsat 39.4 GiB is untouched by package retention and stays on Stage 3. Quotas (/orgs/{org}/quota/packagesexists in the API) are a future guardrail per the runbook, not a cleanup mechanism, and belong in Stage 4.Stage 1 canary result: 29 GB reclaimed from one owner, out of
criticalKai enabled the
coilyco-flight-deck/containerrule and ran thecleanup_packagescron from the admin UI. Rule as applied waskeep_count = 10,remove_days = 7, which is more conservative than the spec above proposed: a version must be both outside the ten most recent and older than seven days.Preview reported 219 versions eligible.
Result
Owner split, showing the change is entirely the treated owner:
One owner gave up 29 GB and 1,835 blobs. Root filesystem is now 6.5 GiB below the critical line for the first time tonight.
The three questions this canary was built to answer
Does retention actually reclaim bytes, given the blob sharing? Yes, decisively. The open worry was that 695 versions sharing 2,465 blobs meant deleting tagged versions would unreference almost nothing. Deleting ~219 version rows removed 1,835 blobs, so sharing was nowhere near total. Retention is the right lever.
Does the blob prune lag behind the rule? No.
unreferenced_blobsis 0 both before and after, and bytes dropped in the same pass, soOLDER_THANdid not gate anything. No second run is needed.Does forgejo#9358 bite at this scale? No. Peak observed CPU was 466m with memory at 1005Mi, restart count held at 21 (unchanged, still 10h old), and the site answered
HTTP 200in 0.028s throughout. The digest-skip fix is evidently present in 16.0.2. Thesha256:rows are exactly why: they are ~58% of version rows and the rule passes straight over them.The 1.6 KiB red herring, recorded
The preview showed the 219 versions at roughly 1.6 KiB each, about 350 KiB in total, which reads as pointless. That figure is the manifest size, not the reclaim. A container version row holds a small JSON manifest and the payload lives in shared blobs, so the preview is structurally unable to show what a deletion frees. It understated the real result by roughly five orders of magnitude.
Worth knowing for Stage 2 planning: the preview's byte column should be ignored entirely. Version count is a weak proxy and manifest size is no proxy at all.
What this predicts for the remaining owners
coilyco-flight-deckwent 64 GB to 35 GB, a 45% reduction, under a deliberately conservative rule. Applying the same shape:coilyco-gamingcontainer - 35 GB, 621 versions,sirens-echodominant at 469. Note it holds more blobs than flight-deck did (5,722) for the same bytes, so its blobs are smaller on average and the ratio may differcoilyco-bridgecontainer - 8163 MB, 138 versions,websitedominant at 121coilyco-gaminggeneric - 46 MB, not worth a run on its ownA comparable outcome puts packages near 47 GiB and root near 79%, back under the warn line.
Recommended next steps
coilyco-gaming/containerandcoilyco-bridge/container, one at a time, running the cron between each so attribution stays clean.data/attachmentsat 39.9 GiB is the second-biggest consumer and no package rule touches it. It has not moved and nothing yet establishes what writes it.lang-*-buildcachemoving tags remain unexamined. Nokeep_countrule touches a moving tag, and each rebuild re-points them, so they are a plausible home for retained blobs. Worth a look once the other owners are done.Nothing else on the cluster was changed.
Stage 1 complete for two owners: root filesystem is
okfor the first time tonightcoilyco-gamingrules applied and the cron run. Cumulative state across tonight's three measurements:57.3 GiB reclaimed in total. Root is 6.2 GiB below the warn line and 31.9 GiB below critical.
Owner split
coilyco-bridgeis unchanged across all three readings, which is a clean control: every byte reclaimed is attributable to a rule that was actually enabled.Note that
coilyco-flight-deckcontinued to shrink from 35 GB to 22 GB during the gaming pass. The post-flight-deck measurement was taken while that cleanup was still completing, so the 29 GB figure in my previous comment understated it. Flight-deck's true contribution is 42 GB, not 29.Version counts
2,039 versions down to 686. The
older_than_7dcolumn is now small everywhere treated, which is the steady state theremove_days = 7guard produces: roughly a week of history per package plus the ten most recent.Health throughout
No degradation at any point across two cleanup passes on a registry that started at 93 GB:
forgejopod restart count held at 21 for the entire operation, unchanged and still 10h oldHTTP 200in 0.025s during the gaming passforgejo#9358 does not apply to 16.0.2 at this scale. Recording that plainly so the next operator does not treat it as a blocker.
What is left
coilyco-bridge/container- the last untreated owner. 521 blobs, 8163 MB, 138 versions,websitedominant at 121. Small, and the only remaining Stage 1 work.data/attachmentsat 39.9 GiB is now effectively tied with packages (43.0 GiB) as the largest consumer, and after bridge it becomes the single largest. It has not moved by a byte all night and no package rule touches it. Stage 3 is now the main event, and nothing yet establishes what writes it.lang-*-buildcachemoving tags, still unexamined.The structural change, which matters more than the bytes
Before tonight the registry grew about 3 GB/day with nothing ageing out and
older_than_30d = 0only because the instance had not been running long enough for anything to reach 30 days. That was unbounded.Retention now caps it. Steady-state package storage becomes a function of publish rate times the retention window rather than a monotonic climb, which is what actually resolves agentic-os#1060 rather than deferring it.
The pressure is gone. The spec in the body is not implemented. Re-scoping rather than closing, because those are two different things and the title only tracks the first.
Live state
70.86%used,status: ok, 72 GB until the 85% critical line. The node'sDiskPressurecondition last transitioned toFalseat2026-08-23T00:35:35Z, eleven minutes after this issue was filed.So the 90.75% in the title is two days stale and nothing is on fire.
What did not cause the relief
forgejo-package-upload-cleanupruns every 15 minutes and is healthy, but it is 31 days old, so it predates this issue and is not the fix. It also cleans up incomplete uploads rather than enforcing registry retention, which is what the body specifies.I did not establish what actually reclaimed the ~20 points. Recording that as unknown rather than guessing.
What is still open
The Stage 1 spec in the body stands on its own merits and is unimplemented. It says plainly that it deletes nothing until Stage 2, so it was never going to relieve pressure by itself. The four
PackageCleanupRulerows atKeepCount = 15are still worth having, because without them the registry has no retention at all and the growth that produced this incident can simply recur.The reasoning in the body is good and I am not relitigating it.
KeepCountalone being sufficient,RemoveDaysbeing a no-op against data witholder_than_30d = 0, and one rule per owner per type being workable becauseKeepCountapplies per package are all specific and checked against the running version.Suggested re-scope
This stops being an incident and becomes planned work: apply the Stage 1 rules, then decide on Stage 2. No urgency at 70% with 140 GB free, but it should not be closed as though the underlying gap were addressed.
Retitling accordingly.
kai-server root at 90.75% critical: Forgejo package registry has no retention rules and is growing ~3 GB/dayto Forgejo package registry has no retention rules: apply the Stage 1 cleanup spec (disk pressure since relieved)