Put the store's tools in a module beside it, since the store's module may not build (ADR 0251)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

This commit is contained in:
jochen
2026-10-08 12:08:02 +02:00
parent f1e2ebce7a
commit f91735b629
2 changed files with 31 additions and 21 deletions
@@ -44,7 +44,10 @@ registry, and pruning of unused images on a machine — each a dry run unless as
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.
2. **A module's own tools, in Go, on the machine that holds the store.** Chosen. Not the store module
itself: a module that provides the artifact store may not build an artifact, because building
publishes to the store, so the tools are a second module, `artifact-store-tools`, assigned beside the
store. The store module's unbuilt TypeScript client goes.
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.
@@ -52,7 +55,7 @@ registry, and pruning of unused images on a machine — each a dry run unless as
**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
store's own container, manifests' content included. 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.
@@ -84,29 +87,33 @@ decides.
## 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:
**1. A module beside the store answers for it, with four tools.** The module `artifact-store-tools`,
assigned to the machine that holds `mesh-artifact-store`, serves a Go bundle. The store module itself
cannot carry it, because a module providing the artifact store may not build an artifact; and the
tools are named for the artifact store, because *store* alone is the database server:
- `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
- `artifact_store_repositories` — every repository, its tags, how many manifests it holds and its size;
- `artifact_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
- `artifact_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`.
- `artifact_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 tools read the store's files, manifests included, through the store's own container and only read
them. The first three
change nothing; `artifact_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
not kept, not yet let go), or — when asked — *collected*. `artifact_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;
- **let go of, yet present** — the controller recorded letting it go and the store still holds it;
- **a named document** — something the controller keeps under a tag (to-be 45 §9);
- **unrecorded** — no record names it.
@@ -116,7 +123,7 @@ 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
count and a time, says what it left; and it is recorded as a hand-act with its `why`. `artifact_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.
@@ -3,7 +3,7 @@ layer: to-be
status: in-progress
code:
- mesh-controller cmd/mesh-controller (the verbs artifacts, collect and images)
- mesh-catalog modules/distribution (the store's tools)
- mesh-catalog modules/artifact-store-tools (the store's tools), modules/distribution (its unbuilt code removed)
- mesh-catalog modules/gitea (package retention)
- mesh-catalog modules/docker (image pruning)
updated: 2026-10-08
@@ -28,9 +28,10 @@ extending [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-na
controller (records)
artifacts ──┐ collect ──┐ images ──┐
│ │ │
store module ─────┴─────────────┘ │
(store_repositories, store_usage, │
store_references, store_collect) │
┌─────────────────┴─────────────┘ │
artifact-store-tools │
(artifact_store_repositories, _usage, │
_references, _collect) │
│ reads the store's files │
│ through its own container │
▼ │
@@ -52,7 +53,8 @@ extending [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-na
The store keeps each blob once, named by its digest, with its size on disk. Each repository lists the
manifests it holds (every one, whether a tag names it or not), its tags, and the layers it may serve.
The tools list those files through the store's own container, read-only, and read each manifest's
content through the store's door. Nothing is written.
content the same way. Nothing is written. The tools are their own module, `artifact-store-tools`,
assigned to the machine that holds the store: the store's module may not build an artifact itself.
From that, a manifest *marks* its own blob, its configuration and each layer, and an index marks the
manifests it lists. The store's nightly collector removes every blob no manifest marks. The tools work
@@ -60,15 +62,15 @@ the same mark, so their numbers are the collector's.
### What each tool answers
- **`store_repositories`** — each repository with its tags, its manifest count and its size: the sum of
- **`artifact_store_repositories`** — each repository with its tags, its manifest count and its size: the sum of
the blobs its manifests mark. A blob shared by two repositories is counted in each, and the answer
says so.
- **`store_usage`** — the store's total size; the largest repositories; the bytes in blobs that more
- **`artifact_store_usage`** — the store's total size; the largest repositories; the bytes in blobs that more
than one repository marks; and the bytes in blobs no manifest marks, which is what the nightly
collector frees next.
- **`store_references`** — for every manifest, one state (below), counted and sized by repository, and
- **`artifact_store_references`** — for every manifest, one state (below), counted and sized by repository, and
listed when one repository is asked about. Then what the records keep that the store does not hold.
- **`store_collect`** — the dry run: what the controller would let go of, and how many bytes the nightly
- **`artifact_store_collect`** — the dry run: what the controller would let go of, and how many bytes the nightly
collector would free once those are gone. A real run with `dry_run` false and a `why` asks the
controller's `collect`, and answers what it did.
@@ -80,6 +82,7 @@ the same mark, so their numbers are the collector's.
| kept — recent build | one of the five most recent builds of a module the mesh holds | no |
| holder of a kept archive | the manifest that keeps a kept archive's blob | no |
| eligible | the mesh made it and keeps it for no reason | yes, through the controller |
| let go of, yet present | the controller recorded letting it go, and the store still holds it | no — said, for a person to look at |
| holder of an eligible archive | the manifest that keeps an eligible archive's blob | yes, with its archive |
| named document | a manifest the controller keeps under a tag (to-be 45 §9) | no |
| unrecorded | no record names it | no — counted and sized only |