See docs/prs/2026-10-15-PR#163-package-de-javagil.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #163 Reviewed-by: mi <1+mi@noreply.git.javagil.de> Co-authored-by: claude-code <claude-code@javagil.de>
106 lines
8.1 KiB
Markdown
106 lines
8.1 KiB
Markdown
# RFC 0002: What Fills a Disk, and When the Volume Runs Out Behind the Quota
|
|
|
|
**Status:**
|
|
- proposed: 2026-09-09
|
|
- accepted: 2026-09-09 (A1 and B1; see Decisions)
|
|
- rejected: -
|
|
|
|
**Proposal:** The System page answers two questions it cannot answer today.
|
|
First, *what* fills a monitored disk — as a breakdown of the categories Werkator itself owns, plus one honest "other" slice for everything it does not, shown in a dialog opened from the disk card.
|
|
Second, that a disk can be physically running out while its quota still has room — by keeping the managed budget as the gauge and drawing the volume's ceiling into it, plus a machine-verdict reason.
|
|
Both were asked for with "this has to be evaluated first", so this RFC is the evaluation; the drafts are meant to be lifted from, not merely looked at.
|
|
|
|
## Context and Problem Statement
|
|
|
|
Since PR#16 the disk gauge shows the *tightest* of three candidate budgets: the user quota, the group quota, and the volume itself ([`DiskQuota.bindingDiskSpace`](../../src/main/kotlin/de/javagil/werkator/metrics/DiskQuota.kt) takes the one with the least free space).
|
|
Since PR#33 there may be several monitored disks, each with its own gauge and its own history series.
|
|
That is enough to say *how full* a disk is, and nothing else.
|
|
|
|
Two gaps follow from it.
|
|
|
|
**A — the gauge says how full, never why.**
|
|
When the SSD gauge turns yellow the only lever available is guesswork: build workspaces, artifacts, unpacked build environments and the checkouts themselves all live under directories Werkator created, and none of them is broken out.
|
|
The one existing number, `repoSizeGib`, is the sum of the primary checkouts and is shown as a tile, so it neither adds up to the gauge nor separates what can be pruned from what cannot.
|
|
|
|
**B — the tightest budget wins silently.**
|
|
Picking the minimum is right for the number, and wrong for the explanation.
|
|
On a Hostsharing Managed Webspace the quota is normally the binding budget, so the gauge shows the quota; if the underlying volume then fills up — through *other* users of that volume, nothing Werkator did — the volume becomes the tighter candidate and the gauge switches to it.
|
|
Three things happen at once, none of them announced: the total jumps from a few hundred GiB to a couple of TiB, the percentage drops although the situation got worse, and the disk's min/max/avg aggregates are reset on purpose (`PersistedMetricsState.diskSourceKey`) so the old ceiling does not survive.
|
|
The reading is correct at every moment and unreadable over time.
|
|
|
|
## A — What fills a disk
|
|
|
|
### What can be measured, and what cannot
|
|
|
|
Werkator can attribute exactly what it created, per repository, all of it below directories it already knows:
|
|
|
|
| Category | Where | Prunable |
|
|
| --- | --- | --- |
|
|
| Build workspaces | `.git/werkator/worktrees/<branchKey>` | yes, by the watcher's stale-worktree pruning |
|
|
| Build environments | `.git/werkator/buildenv/<envKey>/rootfs` | yes, per `envKey` |
|
|
| Artifacts and logs | the artifact store under `XDG_STATE_HOME/werkator/artifacts/<repo>` | yes, by retention |
|
|
| Repositories and history | the checkout and its `.git`, minus the two above | no |
|
|
| Other | the budget's used space minus the sum of the above | not by Werkator |
|
|
|
|
"Other" is the honest part: on a user quota it is the rest of that user's home, on a volume it is every other user of the volume.
|
|
It must be shown, and it must be named as not-Werkator's, otherwise the pie claims an authority it does not have.
|
|
|
|
Two things are deliberately not attributable and should not be faked: a Docker image or volume (owned by the daemon, possibly on another filesystem), and a Gradle cache inside a container.
|
|
|
|
### Cost
|
|
|
|
A breakdown is a directory walk, and a walk is the one expensive thing this collector does.
|
|
It is affordable because the walk *already happens*: `readRepoSizeThrottled` walks every repository and throttles itself to one probe per `REPO_SIZE_PROBE_EVERY_SAMPLES` samples.
|
|
Tallying that same walk into five buckets instead of one sum costs nothing extra; only the artifact store and the `buildenv` directory are new subtrees, and both are shallow.
|
|
On the development machine the full walk of a Werkator checkout with seven worktrees takes well under a tenth of a second warm; on a webspace over network storage it is slower, which is exactly why it must stay on the existing throttle and must never be triggered by opening the dialog.
|
|
|
|
### Drafts
|
|
|
|

|
|
|
|
- **A1 — a dialog from the disk card.** A donut plus a legend with absolute sizes and shares, the header naming the budget and when it was measured. Costs a button in the card and a dialog element; shows enough numbers to act on.
|
|
- **A2 — a stacked bar in the card, no dialog.** Nothing to open, but it takes height in every disk card on a page that already has six gauges, and a 4 % slice is a sliver with no label.
|
|
|
|
### Recommendation
|
|
|
|
**A1**, with the numbers coming from the existing throttled walk and served as part of `GET /api/system` (one `breakdown` per disk), so the dialog is a rendering of data the page already polls rather than a request of its own.
|
|
A donut over a pie: the hole carries the fill percentage, which is the number the card already shows, so the dialog and the card agree at a glance.
|
|
|
|
## B — Quota free, volume full
|
|
|
|
### What the reading should be
|
|
|
|
The three candidates do not compete for one truth; they are three different statements:
|
|
|
|
- the **quota** is what this installation is allowed to use — the budget to manage, and the one a capacity decision is about,
|
|
- the **volume** is what the hardware can still absorb — a constraint, not a budget,
|
|
- the **binding** budget is whichever of them will stop a build first.
|
|
|
|
Today only the third is shown. The fix is to keep showing the budget that is managed, and to show the constraint *in* it.
|
|
|
|
### Drafts
|
|
|
|
- **B1 — the quota keeps the gauge, the volume marks its ceiling.** The bar gains a marker where the volume's free space runs out and a hatched tail for the part of the quota that is no longer backed by free space; the note line names both numbers. One card, one history, no reset.
|
|
- **B2 — two gauges, quota and volume.** Symmetric and unambiguous, but it doubles the disk cards on every host where the volume is never the limit, which is all of them most of the time.
|
|
- **B3 — a reason in the verdict panel only.** No change to the cards: `SystemHealth` gains a rule and the panel says it. Cheapest, and the one place a reader scrolls past first.
|
|
|
|
### Recommendation
|
|
|
|
**B1** (decided; B3 was offered alongside and rejected): the card carries the marker and the note.
|
|
`bindingDiskSpace` keeps deciding what the *level* is — a volume about to fill still has to turn the gauge red — but the *scale* stops moving: the gauge stays on the budget that was binding when the series started, which also retires the aggregate reset.
|
|
|
|
## Decisions
|
|
|
|
Taken on 2026-09-09:
|
|
|
|
1. **A1** — the breakdown is a dialog opened from the disk card, with the donut and the legend. A2 is rejected: the disk cards may not grow.
|
|
2. **The five categories** of the table above, "other" among them and named as not Werkator's.
|
|
3. **B1 alone** — the card carries the marker, the hatched tail and the note line. B3 is rejected for now: the verdict panel keeps the rules it has, and the statement stays where the numbers are. B2 is rejected as a doubling of the cards.
|
|
4. **Per disk**, as the drafts show, because that is what the button sits on. A per-repository split of the same walk answers a different question ("which repository should be pruned") and can be added later inside the same dialog.
|
|
|
|
## Consequences
|
|
|
|
- `SystemMetricsCollector` gains a per-category tally in the walk it already does, and `DiskMetrics` a `breakdown`; nothing new is sampled every 15 seconds.
|
|
- The disk gauge stops resetting its aggregates when the binding source changes, which is a behaviour change of PR#16 and needs its scenario updated.
|
|
- The System page gains its first dialog. It must work without JavaScript to the extent the rest of the page does: the button is rendered only when the data is there, and the dialog is a `<dialog>` element, not a hand-rolled overlay.
|