Record how the registries keep what is named, so nothing piles up unseen (ADR 0251, to-be 51)
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-08
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
---
|
||||
|
||||
# 251. The registries say what they hold and keep what is named, and a machine keeps the image it runs and the one before
|
||||
|
||||
## Context
|
||||
|
||||
The mesh has three places where built things pile up, and only one of them collects.
|
||||
|
||||
**The artifact store collects, and nobody can see it do so.** Since
|
||||
[ADR 0189](0189-the-store-keeps-what-the-records-name.md) the controller lets go of every artifact it
|
||||
made and keeps for no reason, after each build, and the store's collector reclaims the bytes each
|
||||
night. Measured on 2026-10-08 through the controller's `collection` command: 207 archives kept, 207
|
||||
held by a manifest, 0 eligible to let go. That is the records' half. The store's half has no tool at
|
||||
all. The module that holds the store carries a TypeScript client and three tools that no build
|
||||
produces, so nothing on the mesh answers *which repositories are there, how large, and which of what
|
||||
is there no record names*. The one thing ADR 0189 forbids collection to touch — a digest the mesh did
|
||||
not record making — is exactly the thing nobody can count.
|
||||
|
||||
**The package registry never collects.** Every publish of a package to the forge's npm registry adds a
|
||||
version, and nothing removes one. Nothing says which versions any build still resolves to.
|
||||
|
||||
**A machine collects only what nothing names.** The container runtime's weekly prune takes dangling
|
||||
images and unused build cache, and nothing else. An image a machine pulled by digest is not dangling,
|
||||
so every version of every module a machine ever ran stays on it. The build node is the worst case: on
|
||||
2026-10-08 it held 355 images no container used, about 58 GB summed (images share layers, so the disk
|
||||
they take together is less).
|
||||
|
||||
The operator approved three improvements on 2026-10-08: tools for the store, retention for the package
|
||||
registry, and pruning of unused images on a machine — each a dry run unless asked otherwise.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**For the store's tools.**
|
||||
|
||||
1. *The store seat's verbs.* The tools a role answers are the seat's, and a second holder of the store
|
||||
would serve them too. But a verb added to a held seat whose holder lives in another repository lands
|
||||
in three steps ([ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md)), and the
|
||||
store has one holder and no second in view. Not now; the tools can be promoted when a second holder
|
||||
exists.
|
||||
2. **The store module's own tools, in Go.** Chosen. They replace the TypeScript client nothing builds.
|
||||
3. *The controller's verbs.* The controller holds the records but not the store's files; the store may
|
||||
move to a machine the controller is not on. Rejected for everything except what only the records can
|
||||
answer.
|
||||
|
||||
**For what the store tools read.** The store's door lists repositories and tags, but not a manifest no
|
||||
tag names — and since the mesh pins by digest, that is almost every manifest. Only the store's files
|
||||
list them, with each blob's size. So the tools read the store's own files, read-only, through the
|
||||
store's own container, and read each manifest's content through the door. The registry's own
|
||||
`garbage-collect --dry-run` was weighed and rejected as the source: it says what is unmarked, but not
|
||||
how large, and not what letting go of a manifest would free.
|
||||
|
||||
**For collecting on demand.** *Letting the store tool delete what the records do not keep* was rejected:
|
||||
the controller deletes and records that it did, and two deleters means two records. **The same sweep
|
||||
as after a build, asked on demand,** is chosen.
|
||||
|
||||
**For what is unrecorded.** *Removing by age what no record names* was rejected: it is the rule of ADR
|
||||
0189 §3 broken, and the images genesis pushed are exactly that. The tools count it and size it; a person
|
||||
decides.
|
||||
|
||||
**For the package registry.**
|
||||
|
||||
1. *Keep the newest N of each package, and nothing else.* Rejected: a range pinned to an older line
|
||||
(`^0.1.0` while `0.2.x` is the newest) resolves to a version this drops.
|
||||
2. *Keep what lockfiles name.* Not enough alone: a module without a lockfile resolves its range at
|
||||
build time.
|
||||
3. **Keep what is referenced, and the newest N.** Chosen. Referenced means a version a lockfile on a
|
||||
repository's default branch names, the highest version satisfying a range a package manifest there
|
||||
names, and a version a dist-tag names.
|
||||
|
||||
**For a machine's images.**
|
||||
|
||||
1. *`docker image prune --all`.* Rejected: it removes the previous version, so going back needs a pull,
|
||||
and it removes the image of a scheduled step between two of its runs.
|
||||
2. *By age alone.* Rejected: a module not rebuilt for a while would lose its previous version.
|
||||
3. **Keep what a container uses, what the machine's declaration names, and the newest other image of
|
||||
each line.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. The store module answers for the store, with four tools of its own.** The distribution module,
|
||||
holder of `mesh-artifact-store`, serves a Go bundle in place of its unbuilt TypeScript:
|
||||
|
||||
- `store_repositories` — every repository, its tags, how many manifests it holds and its size;
|
||||
- `store_usage` — the store's size, the largest repositories, what blobs are shared, and what the
|
||||
store's nightly collector would free now;
|
||||
- `store_references` — what the records say of each manifest the store holds (below), and what the
|
||||
records keep that the store does not hold;
|
||||
- `store_collect` — a dry run unless `dry_run` is false; a real run needs `why`.
|
||||
|
||||
The tools read the store's files through the store's own container and only read them. The first three
|
||||
change nothing; `store_collect` changes nothing either, unless it is a real run, and then only by asking
|
||||
the controller (point 3).
|
||||
|
||||
**2. The records are asked, never copied.** A new controller verb, `artifacts`, answers every
|
||||
reference the mesh recorded making, with its state: *kept* because a definition names it, *kept*
|
||||
because it belongs to one of the five most recent builds of a module the mesh holds, *eligible* (made,
|
||||
not kept, not yet let go), or — when asked — *collected*. `store_references` sets that beside what the
|
||||
store holds and says, for every manifest, one of:
|
||||
|
||||
- **kept**, with the reason;
|
||||
- **a holder of a kept archive** (the manifest that keeps an archive's blob, [issue 253](../04-ISSUES/253-the-stores-collector-would-delete-every-archive-the-mesh-keeps/00-report.md));
|
||||
- **eligible**, or a holder of an eligible archive;
|
||||
- **a named document** — something the controller keeps under a tag (to-be 45 §9);
|
||||
- **unrecorded** — no record names it.
|
||||
|
||||
Unrecorded manifests are counted and sized, by repository. **They are never removed by any tool in this
|
||||
record.** ADR 0189 §3 stands as written.
|
||||
|
||||
**3. Collecting on demand is the same sweep, asked by a person.** A new controller verb, `collect`,
|
||||
says what the sweep would let go of, and does it only with `confirm` and a `why`. A real run holds every
|
||||
kept archive before it lets anything go, exactly as the sweep after a build does; it is bounded by a
|
||||
count and a time, says what it left; and it is recorded as a hand-act with its `why`. `store_collect`'s
|
||||
dry run adds what only the store knows: how many bytes the nightly collector would free once those
|
||||
manifests are gone, beside what it would free tonight anyway. Its real run asks `collect`. Bytes are
|
||||
still reclaimed by the nightly collector, because only it runs with the store held still.
|
||||
|
||||
**4. The package registry keeps what is referenced and the newest five of each package.** The forge
|
||||
module, holder of `npm-package-registry`, gains a Go bundle beside its TypeScript one, with two tools:
|
||||
|
||||
- `npm_packages` — every package of the registry's owner, each version with its publish date and size,
|
||||
and why each is kept;
|
||||
- `npm_retention` — what retention would delete and how many bytes; a dry run unless `dry_run` is
|
||||
false, and a real run needs `why`.
|
||||
|
||||
A version is kept when:
|
||||
|
||||
- a lockfile on the default branch of any repository on the forge names it;
|
||||
- it is the highest version satisfying a range that a package manifest there names;
|
||||
- a dist-tag names it; or
|
||||
- it is among the newest five of its package (`keep` changes the number; it is never below one).
|
||||
|
||||
**A repository whose files could not be read stops a real run before anything is deleted.** Retention
|
||||
that does not know what is referenced does not delete.
|
||||
|
||||
**5. A machine removes images no declaration uses, and keeps the one each module runs and the one
|
||||
before.** The container runtime's module gains `docker_prune_images`. It keeps:
|
||||
|
||||
- every image a container on the machine uses, in any state;
|
||||
- every image the machine's declaration names — both what the mesh would send it now and what it was
|
||||
last sent — asked of a new controller verb, `images`;
|
||||
- for each line of images, the newest image besides those. A line is the images sharing a repository
|
||||
name, joined across names when one image carries several (a build's local tag and the store's name);
|
||||
- every image younger than a floor, seven days by default — the same week the weekly prune leaves.
|
||||
|
||||
It removes the rest by id, without force, so the runtime itself refuses an image a container uses. **It
|
||||
refuses to remove anything when the controller cannot say what the declaration names.** It is a dry run
|
||||
unless `dry_run` is false, and a real run needs `why`.
|
||||
|
||||
**6. Nothing here runs by itself.** No timer is added: the store's nightly collector and the weekly
|
||||
dangling prune stay as they are. Each tool is a dry run until a person runs it for real, and whether any
|
||||
of them becomes scheduled is a later decision, made from what their dry runs said.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The question "what is in the store that nothing records" has an answer, with a size. Before, it could
|
||||
only be estimated by reading the store's disk by hand.
|
||||
- Each controller verb answers in one bus message, and an answer larger than the bus carries is lost
|
||||
on its way back (issue 314, open while this was written). So `artifacts` leaves out the collected references unless asked,
|
||||
and `images` answers only images.
|
||||
- Five builds are kept by two rules now: the store's (ADR 0189 §3) and the package registry's. They
|
||||
agree on purpose, so "how far back can this go" has one answer.
|
||||
- On a machine, the previous version of a module is a local image, and the one before that is a pull
|
||||
from the store. The store keeps five, so a pull succeeds.
|
||||
- A build node removes base images no declaration names, and pulls them again at its next build. That
|
||||
is a cost in time, and it is visible in the dry run before anybody accepts it.
|
||||
- The store module stops carrying code no build produces, and the `image.pushed` event it declared
|
||||
and never emitted goes with it.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **The controller.** `artifacts` names every reference the keep set holds with its reason, and every
|
||||
eligible reference; a collected reference only when asked. `collect` without `confirm` deletes nothing,
|
||||
against a fake store that records what it was asked; with `confirm` and no `why`, it is refused; a
|
||||
real run holds kept archives before letting anything go. `images` names every container image of a
|
||||
declaration, and of the last one sent.
|
||||
- **The store's tools.** Against a fake store and a fake listing of its files: classification of each
|
||||
manifest (kept, holder, eligible, named document, unrecorded); the bytes the collector would free,
|
||||
counting a blob shared with a kept manifest as kept; a dry run deletes nothing; a real run without
|
||||
`why` is refused.
|
||||
- **Package retention.** Range satisfaction (`^`, `~`, exact, `x`, comparison, `||`, hyphen); a version
|
||||
a lockfile names is kept, whatever its age; the newest five are kept; an unreadable repository stops
|
||||
a real run; a dry run deletes nothing.
|
||||
- **Image pruning.** Against a fake runtime: what a container uses, what the declaration names, the
|
||||
newest other image of each line and anything under the floor are kept; an image with two names is one
|
||||
line; an unanswered controller refuses; a dry run removes nothing; removal is never forced.
|
||||
- **Live.** The dry runs against the running store, the package registry and each machine, read before
|
||||
anything is run for real. Their numbers are recorded in to-be 51.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0189 — the store keeps what the records name](0189-the-store-keeps-what-the-records-name.md)
|
||||
- [ADR 0246 — a seat's new verb is promised before it is required](0246-a-seats-new-verb-is-promised-before-it-is-required.md)
|
||||
- [ADR 0109 — a package registry seat is one per ecosystem](0109-a-package-registry-seat-is-one-per-ecosystem.md)
|
||||
- [ADR 0156 — an artifact is what a build produces](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||
- [issue 253 — the store's collector would delete every archive the mesh keeps](../04-ISSUES/253-the-stores-collector-would-delete-every-archive-the-mesh-keeps/00-report.md)
|
||||
- [to-be 51 — the registries keep what is named](../03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md)
|
||||
Reference in New Issue
Block a user