From 59bc551b5bbc3f63aa9a25b051f751c93889d2a8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 14:08:45 +0200 Subject: [PATCH 1/3] ADR 0257, issues 321-323, research 035: record the bases builds copy, and say what the pre-mesh images are The store's tools counted 23.8 GiB nothing records: copied bases the sweep could never let go of, platforms a let-go index left behind, and images from before the mesh. Package retention read no lockfile, so it kept versions by age alone. --- .../00-overview.md | 85 +++++++ .../01-the-images.md | 64 ++++++ ...-vendor-image-is-a-declared-build-input.md | 7 + ...hat-stood-on-it-and-held-once-per-image.md | 212 ++++++++++++++++++ .../51-the-registries-keep-what-is-named.md | 57 ++++- .../00-report.md | 36 +++ .../01-diagnosis.md | 55 +++++ .../00-report.md | 53 +++++ .../00-report.md | 38 ++++ 9 files changed, 604 insertions(+), 3 deletions(-) create mode 100644 01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md create mode 100644 01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md create mode 100644 02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md create mode 100644 04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md create mode 100644 04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md create mode 100644 04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md create mode 100644 04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md diff --git a/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md b/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md new file mode 100644 index 00000000..ee180334 --- /dev/null +++ b/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md @@ -0,0 +1,85 @@ +--- +status: active +initiated: 2026-10-08 +touches: + - 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md + - 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md + - 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md + - the artifact store, the catalogue's modules that pin an image by digest +--- + +# 035 — The images from before the mesh + +## What is investigated + +The artifact store's tools ([ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)) +counted 469 unrecorded manifests marking 23.8 GiB on 2026-10-08. 374 of them are copied bases +([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md), +[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)). +Most of the rest are images pushed to the same store before the mesh existed. The predecessor's +pipeline built them, under a namespace per owner, one repository per app. + +No record of the mesh names these images, so [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) +§3 keeps every tool away from them. That is correct: the mesh did not make them. It also means they stay +until a person decides. This effort finds out, for each one, whether anything still uses it, and +recommends what to do. Nothing was deleted. + +## How it was measured + +All of this was read, nothing was written. On 2026-10-08: + +- **The store's view.** `artifact_store_references` and `artifact_store_repositories` give each + repository's manifests, tags and sizes. They give no dates. +- **What each machine declares.** The controller's `images` verb answers for each of the four machines. +- **What each machine runs.** The container runtime module's container list answers on each machine. +- **What still names them.** I searched every repository on the forge for each image's path and digest. + There are 50 repositories, and I read 31 of them closely: both catalogues, the apps' own repositories, + the setup repositories and the predecessor's. +- **Dates.** No tool gives a manifest's push time. A date below is either an image's `created` date, from + a copy still in a machine's local image store and matched by tag, or the date of the commit the tag + names. Where neither exists, the date is not known, and the table says so. + +## What was found + +[01-the-images.md](01-the-images.md) has the table. In summary: + +- **3 repositories hold an image a machine runs today.** Each is one of three app images on the anchor: + an invoicing app's API, its web front, and a static company website. A catalogue module pins each by + digest, under an older name of the same store. Each repository also holds two older tags that + nothing uses. +- **2 repositories are pinned only by catalogue modules that no machine is assigned.** One is a + magazine site's older image. The other is an older photo client, pinned by two per-person photo + modules. Both are stale definitions. +- **35 repositories, plus 2 single manifests in repositories the mesh builds, are used by nothing.** + Nothing runs them, declares them or pins them. + - Most were superseded by a module that builds its own image of the same app. + - The rest are abandoned: a trading bot, a chatbot, test images, the predecessor's dashboard and flow + engine, and the bootstrap's first builder and controller images. + - Together they mark between 5.9 and 7.5 GiB that nothing else marks. The two bounds depend on how the + blobs they share among themselves are counted. + +**A finding beside the question.** The store's tools classify the three running images as +*unrecorded*, the same word they use for an image nothing uses. The reason is that the catalogue pins +them under an older name of the store, and the mesh did not make them. Nothing removes an unrecorded +manifest today, so nothing is at risk. But any later decision to let unrecorded images go by some +other rule would take running images with it. That is recorded as +[issue 323](../../04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md). + +## What is recommended + +- **Keep** the three running images. Point their modules at an image the mesh builds, where one exists, + so the running images become the mesh's own. Two of the three apps already have a build of their own + on a branch. +- **Delete the two stale catalogue modules**, then treat their images like the rest. +- **Let go of the 35 repositories and 2 manifests** once a decision says how an image the mesh did not + make may be removed. ADR 0189 §3 forbids every tool from doing it today, and that is intended. The + decision is the operator's: the images are the operator's, and some can no longer be rebuilt. +- **Adopt none of them as a module.** Every app in use already has a module. + +## Open questions + +- Should an image the mesh did not make be removed through the controller's records, by a person + recording it (as ADR 0257 lets a person record a copied base), or through a separate verb of the + store's tools? +- Should the older tags in a repository that is in use (about 0.8 GB together) go before their modules + are moved to the mesh's own build, or with it? diff --git a/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md b/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md new file mode 100644 index 00000000..257a4e71 --- /dev/null +++ b/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md @@ -0,0 +1,64 @@ +# 035 — The images, one by one + +Read on 2026-10-08. Sizes are the bytes a repository's manifests mark. *Only theirs* is what no other +manifest in the store marks, which is what removing the repository would free. A machine is named by its +role. + +Dates: *created* is an image's build date, from a copy in a machine's local image store matched by tag; +*commit* is the date of the commit the tag names; *not known* means neither was found. + +## In use + +| what | size | manifests | newest | in use by | named by | recommendation | +|---|---|---|---|---|---|---| +| an invoicing app's API | 1.41 GB | 3 tags | created 2023-01 | the anchor, its API container (newest tag, matched by digest) | its catalogue module, pinned under an older name of the store; also the frozen runtime base of the app's own build on a branch | keep the newest; let the two older go (about 0.7 GB); move the module to the mesh's own build | +| the same app's web front | 60.5 MB | 3 tags | created 2023-01 | the anchor, its web container (matched by digest) | its catalogue module, likewise | keep the newest; let the two older go | +| a static company website | 474 MB | 3 tags | created 2022-08 | the anchor, the website container (matched by digest) | its catalogue module, likewise; the site's repository has its own build on a branch, not yet delivered | keep the newest; let the two older go (about 75 MB) | + +## Pinned only by a stale module + +| what | size | manifests | newest | in use by | named by | recommendation | +|---|---|---|---|---|---|---| +| a magazine site, its older image | 393 MB | 3 tags | created 2025-10 | nothing: the anchor runs the mesh-built image | a catalogue module whose definition no machine uses | let the two older go; the newest once the stale module is deleted | +| a photo app's older client | 11 MB | 1 | created 2024-10 | nothing: the anchor runs the mesh-built client | two per-person photo modules, assigned to no machine | delete the two modules, then let it go | + +## Used by nothing + +Each row is one repository, unless it says otherwise. + +| what | size (only theirs) | manifests | newest | superseded by, or why abandoned | recommendation | +|---|---|---|---|---|---| +| a car-listing scraper | 2.31 GB (1.43 GB) | 3 | commit 2026-02 | the module's own build, which the home server runs | let go | +| a chatbot's API | 3.04 GB | 1 | before 2024-09 | abandoned since 2024 | let go | +| a chatbot's web front | 19.8 MB | 1 | before 2024-09 | abandoned | let go | +| a comic app's API and web front (2 repositories) | 828 MB | 1 each | created 2026-09 | the predecessor's; no module | let go, unless the app is wanted back (then build it as a module) | +| a photo app's earlier API and web front (2 repositories) | 519 MB | 4 each | not known | the photo modules | let go | +| a photo app's server and admin client (2 repositories) | 445 MB | 1 each | created 2024-10 | the photo modules' own builds | let go | +| a map game's API and web front (2 repositories) | 121 MB | 3 each | commit 2026-02 | renamed to the travel game below | let go | +| a travel game's API and web front (2 repositories) | 104 MB | 2 each | commit 2026-02 | no containers or data remain | let go (it can be rebuilt from its repository) | +| a racing-circuit site | 264 MB | 3 | created 2026-06 | the module's own build, which the home server runs | let go | +| an older company website | 400 MB | 1 | not known | the static website above, and its own build | let go | +| a payment-link site | 52.6 MB | 3 | before 2025-11 | the module's own build | let go | +| a mail-forwarding service | 58.5 MB | 1 | created 2026-03 | its repository's own module, assigned nowhere | let go | +| a snake game and a text game (2 repositories) | 88 MB (0.8 MB) | 1 each | created 2026-08 and 2026-09 | the modules' own builds | let go | +| a trading bot | 85 MB | 3 | commit 2022-08 | abandoned | let go | +| a trading bot's scheduled jobs and web front (2 repositories) | 83 MB | 1 each | not known | abandoned (setup last changed 2025-01) | let go | +| three base images of a former owner namespace (3 repositories) | 188 MB | 1 each | not known | abandoned (setup last changed 2023-05) | let go | +| test images (3 repositories) and a capability test | 135 MB | 1 to 3 | not known | tests of the predecessor | let go | +| the predecessor's dashboard and flow-engine API (2 repositories) | 165 MB (157 MB) | 2 and 3 | created 2026-09 | the predecessor is retired | let go | +| the bootstrap's first builder and controller images (2 repositories) | 30 MB (26 MB) | 1 each | created 2026-09 | the build agent; the controller runs as a bundle | let go | +| a build agent's and the controller's former server images (2 repositories) | 62 MB (55 MB) | 2 each | created 2026-09 | the same | let go | +| one manifest in the mesh-built tools image's repository | 87 MB (6.6 MB) | 1 of 4 | not known | the three recorded beside it | let go | +| one manifest in the route proxy's server repository | 9.9 MB (6.0 MB) | 1 of 6 | not known | the kept build the machines declare | let go | + +**Together**, the rows used by nothing mark between 5.9 GiB and 7.5 GiB that nothing outside them marks. +The upper bound sums each repository's own bytes. That counts twice a blob two of them share. The lower +bound subtracts every shared blob, and that also subtracts blobs shared only among these rows, which +removing all of them would free. Removing the two stale modules' images adds 0.4 GiB. Removing the older +tags of the three images in use adds about 0.8 GB. + +**Not determined.** + +- A push date for 14 of the repositories. +- Whether the forge listing of 50 repositories is all of them. A second page came back empty, so I think + it is. diff --git a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md index 2237aeac..066a53fc 100644 --- a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md +++ b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md @@ -36,6 +36,13 @@ image the manifest did not declare is refused before the build, naming the image its own stages, declared arguments and `scratch` are not fetches. An unpinned vendor image is refused: a tag is what somebody else can move. +> **The mechanism changed — 2026-10-08, by [ADR 0257](0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md).** +> A declared image is still copied into the mesh's registry before the build, and the recipe is still handed +> the copy. What moved is where the copy goes. It no longer goes "under the module's repository" +> (`/on-`). It goes into one repository per upstream image, `upstream//`, +> whichever modules stand on it. Each copy is also recorded where it is made, so the store's sweep can +> let it go. + A recipe whose `FROM` names an undeclared base was at first said, not refused: the mesh's own images — the control plane's, the tool runtime's, the route proxy's — started from a public base and declared none, and refusing those refuses genesis. *Amended the same day:* those three declare diff --git a/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md new file mode 100644 index 00000000..f11f461a --- /dev/null +++ b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md @@ -0,0 +1,212 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-08 +deciders: jochen +reconstructed: false +supersedes-in-part: + - 0097-a-vendor-image-is-a-declared-build-input.md +extends: 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md +--- + +# 257. A copied base is recorded where it is copied, kept by the builds that stood on it, and held once per image + +## Context + +A module's build may stand on an image published elsewhere, declared in its manifest and pinned by digest +([ADR 0097](0097-a-vendor-image-is-a-declared-build-input.md)). Before the build, the builder copies that +image into the artifact store, whole, between registries +([ADR 0096](0096-an-upstream-image-is-copied-between-registries.md)), under the module's own repository, +`/on-`. This record calls such a copy a **copied base**. + +[ADR 0251](0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +gave the store tools that classify every manifest against the controller's records. Their first reading, +on 2026-10-08, found 469 manifests marking 23.8 GiB that no record names, so nothing would ever remove +them ([ADR 0189](0189-the-store-keeps-what-the-records-name.md) §3). Issue +[321](../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md) +has the details. 374 of those manifests, in 27 repositories, are copied bases. Four facts explain them: + +1. **The copy is recorded as something the build stood on, never as something it made.** The records + name what each build *made*, and the sweep reads only that. A build's `built_against` does name the + copy, but the sweep does not read it. +2. **A build that fails after copying records the copy nowhere.** A failed build keeps no module and no + `built_against`, yet the copy is in the store from the moment it is made. +3. **An upstream image is an index over several platforms.** Each platform manifest is put under its own + digest, so each is a manifest of the repository in its own right. The store's collector keeps every + manifest a repository holds. Letting the index go therefore frees none of the bytes of the images it + names. This is true of every upstream artifact the mesh has ever let go of, not only of copied bases. +4. **There is one copy per module, not one per image.** Several modules standing on one image hold + several copies. Their bytes are stored once, because the store keeps each blob once whichever + repositories link it, so the copies cost little disk. But each module's first copy asks the public + registry for the whole image again, and that registry rations anonymous pulls. + +The largest single case is a route proxy's Go toolchain image: 34 manifests and 10.4 GiB in one +repository. That is two versions of the toolchain image, each with every platform it is published for, +Windows included. + +## Considered Options + +**What records a copy.** + +1. *Read `built_against` as the record.* Rejected as the only record, because it misses fact 2: a + failed build has none. It is kept as the source of the backfill (point 2 below). +2. **A record of its own, written when the build reports, whether the build worked or not.** Chosen. + The builder says what it copied in every outcome it announces. The controller keeps one row per copy. + +**What keeps a copy.** + +1. *A definition naming it, as for an artifact.* Rejected. A definition names the image upstream, by the + digest that every copy of that image shares. As a reason it would keep every module's copy of an image + for as long as any module stood on it. And no machine is ever handed a base, so a definition is never + the reason a base is needed. +2. *Its own five most recent copies.* Rejected: a copy has no builds of its own to count. +3. **The kept builds that stood on it.** Chosen. A copy is kept while one of the five most recent + successful builds of a module the mesh holds stood on it. That is the build ADR 0189 already keeps for + a release to go back to, and it is the one a rebuild would need the copy for. + +**Where a copy lives.** + +1. *Under the module, as now.* Rejected for fact 4. +2. **One repository per upstream image, named for the image: `upstream//`.** Chosen. The + first module to stand on an image copies it, and every later module finds it already held. Two images + with one path on different hosts are not confused. On the move, a copy the module's own repository + already holds is taken from there: its manifests are read from the store and its blobs are mounted + across. Upstream is not asked. + +**What becomes of an index's platforms.** + +1. *Let the index go alone.* Rejected for fact 3. +2. *Let the index go with every platform manifest it names.* Rejected, because a kept index of the same + repository may name one of them. +3. **Let the index go with the platform manifests it names that no kept index of the repository also + names, platforms first.** Chosen. The controller reads only its records, so it cannot see an + unrecorded index that also names a platform. The store tools can see that, and their dry run lists + every manifest that stays and would lose a platform, before anybody runs collection for real. + +**The copies no record names**, made before this record by a failed build, or before builds kept what +they stood on. + +1. *Remove every manifest in a copy's repository by name.* Rejected: ADR 0189 §3 keeps removal to what a + record names, and a repository's name is not a record. +2. *Leave them for ever.* Rejected: they are most of what the store carries for no reason. +3. **A person records them, and the sweep then decides as for every other copy.** Chosen. The store + tools list each unrecorded copy at the top of a copy's repository, with what it alone marks. A new + controller verb, `mirrors`, records the ones a person names, with a reason. It refuses any reference + outside a repository only the copy step writes, and any reference the store does not hold. Recording + deletes nothing. + +**Copying only the platforms the mesh runs** was weighed and is left for a later record. It would remove +most of the route proxy's toolchain copy, which is Windows images, but the reference a build is handed +would then no longer be the upstream digest. Whether the store accepts an index whose platforms are +partly absent depends on its version and settings, and that has not been measured. + +## Decision + +**1. One repository per upstream image.** A copied base is put in `upstream//` (the public +hub is `docker.io`, and a port's colon becomes a dash). The module's own repository from before is +offered as a source: where it holds the image by digest, the copy is made from it, and nothing is asked +of upstream. A module's build arguments name the new copy. Its source fingerprint is unchanged, because +the fingerprint reads a base by digest, never by where it was copied to. + +**2. A copy is recorded where it is copied.** The builder says, in every build outcome, what it copied. +It says so when the build fails too. The controller keeps one row per copy in `artifact_mirrored`: the +build that first said it, or a person's reason. A migration records every copy that the existing build +records already name in a module's `/on-` repository, each by the earliest build that +named it. Only the copy step ever wrote a repository so named. + +**3. A copy is kept by the kept builds that stood on it, and by nothing else.** A copy is kept, with the +reason `stood-on`, while one of the five most recent successful builds of a module the mesh holds stood +on it. Otherwise it is eligible, and the sweep lets it go as it lets any eligible artifact go. A +definition never keeps a copy. A copy already let go of stays let go of. The reason `stood-on` belongs to +copies alone: another module's artifact is still kept by that module's own builds. + +**4. An index goes with its platforms.** When the sweep lets an index go, it first deletes the platform +manifests the index names that no kept index of the same repository names, and deletes the index last. A +sweep cut short leaves the index to be asked about again. If the sweep cannot read which platforms the +kept indexes name, it lets nothing go. This applies to every index the mesh let go of, upstream artifacts +included. + +**5. A person may record a copy that no record names.** The controller's `mirrors` verb lists the copies +the records hold, with their states. Given references, it records the ones that are in a repository only +the copy step writes and that the store holds. It is a dry run without `confirm`. With `confirm` it needs +a `why`, and it is recorded as a hand act. It deletes nothing. `artifact_store_references` lists the +candidates in `unrecorded_mirrors`, largest first. + +**6. The store tools count a platform as its index's.** A platform manifest takes the state of the +recorded index of its repository that names it, the more keeping state when two do. An index no record +names leaves its platforms unrecorded with it. `artifact_store_collect` counts the platforms that go with +an index, and its dry run lists, in `platforms_named_by_what_stays`, every manifest that stays and names a +platform that would go. + +ADR 0097 is superseded in one sentence only: a base is copied into the image's own repository, no longer +"under the module's repository". Everything else ADR 0097 decided stands. + +## Consequences + +- The records answer for every copy a build makes from now on, and the sweep keeps or lets go of copies + by the same rule as the artifacts. +- **Recording the copies frees little by itself.** Measured on 2026-10-08 against the records: + - 17 of the 23 recorded copies were stood on by a kept build. + - The 6 that were not hold about 0.4 GiB that only they mark. + - Most copies share their bytes with a kept copy of the same image, so letting them go frees their + manifests, not their bytes. + - Most of the 23.8 GiB is copies no record names, and pre-mesh images (research + [035](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)). Those wait for a person, + through `mirrors` and, for the pre-mesh images, a decision of its own. +- The move to one repository per image costs one copy per image, taken from the store itself. After it, + each module's former copies stop being stood on, become eligible after five more builds of that + module, and go, freeing their manifests. +- A module named `upstream` would collide with the copies' namespace. The build edges already ignore + the copies' repositories, so no edge is drawn to such a module. Refusing the name is left to the + catalogue's module check, and it is not done here. +- The kept copy of the route proxy's toolchain still holds its Windows platforms. I estimate them at + about 4.9 GB, from the two platform manifests of each version that are larger than 2 GB; which index + names which platform was not read. Removing them is for the later record on copying only the platforms + the mesh runs. + +## How it is checked + +- **The builder.** + - The repository is named for the image: the hub, other hosts, a port, upper case. + - A copy held under the module's former repository is mounted, every blob, with nothing uploaded + again and nothing asked of an upstream that is gone. + - A second module standing on the image copies nothing. + - A failed build and a working build both say what they copied. +- **The records.** + - A copy only a failed build made is eligible. + - A copy is kept, with `stood-on`, by the five kept builds and by nothing else, and a definition + naming the image keeps no copy. + - Another module's artifact is not kept as a copy. + - A collected copy stays collected. + - A person's record stands over a later build's. + - The migration's backfill records one copy, by its earliest build, and records nothing outside an + `on-` repository. + - A copy's repository draws no build edge. +- **The sweep.** + - Against a fake store, an index goes with its own platforms, platforms first. + - A platform a kept index names stays. + - An image is one delete. + - A store that cannot say what a kept index names keeps the index. +- **The `mirrors` verb.** + - It is composed only with what it takes. + - It takes only references in a copy's repository. + - A dry run records nothing. + - A real run records what the store holds, and the sweep then offers it. + - It never asks the store anything but HEAD. +- **The store tools.** Over a view with two indexes of one image and an unrecorded copy: + - A platform takes its index's state, the more keeping state when two indexes name it. + - The unrecorded copy's top is offered with what it alone marks. + - Letting the old index go takes only its own platform. + - Sparing nothing would be said as a kept index losing a platform. +- **Live.** After rollout, `mesh-controller.collect` (dry run), `mesh-controller.mirrors` and + `artifact_store_references` are read. Their numbers are recorded in to-be 51. + +## References + +- [ADR 0096 — an upstream image is copied between registries](0096-an-upstream-image-is-copied-between-registries.md) +- [ADR 0097 — a vendor image is a declared build input](0097-a-vendor-image-is-a-declared-build-input.md) +- [ADR 0189 — the store keeps what the records name](0189-the-store-keeps-what-the-records-name.md) +- [ADR 0251 — the registries say what they hold and keep what is named](0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +- [issue 321 — a copied base was recorded by nothing, and a let-go index kept its platforms](../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md) +- [research 035 — the images from before the mesh](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md) +- [to-be 51 — the registries keep what is named](../03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md) diff --git a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md index 86518bf0..66a90111 100644 --- a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +++ b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md @@ -2,7 +2,7 @@ layer: to-be status: in-progress code: - - mesh-controller cmd/mesh-controller (the verbs artifacts, collect and images) + - mesh-controller cmd/mesh-controller (the verbs artifacts, collect, images and mirrors), internal/builder (copied bases), internal/inventory (artifact_mirrored) - 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) @@ -10,6 +10,7 @@ updated: 2026-10-08 decisions: - 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md - 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md + - 02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md --- # 51 — The registries keep what is named @@ -80,6 +81,7 @@ the same mark, so their numbers are the collector's. |---|---|---| | kept — definition | a module's recorded definition names it | no | | kept — recent build | one of the five most recent builds of a module the mesh holds | no | +| kept — stood on | a copied base one of those builds stood on (ADR 0257) | 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 | @@ -90,7 +92,32 @@ the same mark, so their numbers are the collector's. A holder is recognised by its shape: a manifest with an empty configuration and one layer, which is the blob of an archive the records name. -### The controller's two verbs for the store +**A platform manifest is its index's** ([ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)). +An upstream image is an index over several platforms, and each platform manifest is a manifest of the +repository in its own right. A platform manifest takes the state of the recorded index of its +repository that names it, and the more keeping state when two indexes name it. An index that no record +names leaves its platforms unrecorded with it. + +### Copied bases + +A build that stands on an image published elsewhere copies it into the store first. Since +[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md): + +- **One repository per upstream image**, `upstream//`, whichever modules stand on it. A copy + that a module's former repository, `/on-`, already holds is copied from there, with + each blob mounted across, and upstream is not asked. +- **Recorded where it is copied.** The builder says what it copied in every build outcome, including a + failed one, and the controller keeps a row per copy in `artifact_mirrored`. The copies the existing + build records already named were recorded by the migration that made the table. +- **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never + keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. +- **An index goes with its platforms.** The sweep deletes the platform manifests an index names that + no kept index of its repository also names, and deletes the index last. +- **A copy no record names** waits for a person. `artifact_store_references` lists each one in + `unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a + person names, and the sweep then decides about them. + +### The controller's verbs for the store - **`artifacts`** reads only. It answers every reference the mesh recorded making, with its state and, for kept ones, the reason; collected ones only when `collected` is asked for; narrowed to one @@ -98,7 +125,15 @@ the blob of an archive the records name. - **`collect`** says what the sweep would let go of. With `confirm` and a `why` it runs the sweep that runs after a build: every kept archive held first, then each eligible reference let go and recorded as collected, stopping at the first refusal by the store. It is bounded by a count and a time, says - what is left, and is a hand-act recorded with its `why`. + what is left, and is a hand-act recorded with its `why`. An index is let go of with its own platform + manifests (ADR 0257). +- **`mirrors`** lists the copied bases the records hold, with their states. Given references, it records + the copies that are in a repository only the copy step writes and that the store holds. It is a dry + run without `confirm`; with `confirm` it needs a `why`, and it is a hand act. It deletes nothing. + +`artifact_store_collect` counts the platforms that go with an index, and its dry run lists, in +`platforms_named_by_what_stays`, every manifest that stays and names a platform that would go. The +controller cannot see such a manifest, because it reads only its records. ## The package registry @@ -118,6 +153,13 @@ repository on the forge. Ranges are read as npm reads them: exact, `^`, `~`, `x` and `*`, comparisons, hyphen ranges and `||`. A range that cannot be read is named, and its package keeps every version until it can. +Lockfiles are read in all three of their versions: version 1's nested `dependencies`, and the `packages` +of versions 2 and 3, where an entry's own `dependencies` is a map of names to ranges. A file that could +not be read whole is named, and like an unread repository it stops a real run before anything is deleted +([issue 322](../../04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md)). +Every version is measured from the forge's file sizes, and a size that could not be read is said, never +counted as zero. + ## A machine's images The container runtime module's tool `docker_prune_images` works on the machine it runs on. @@ -150,3 +192,12 @@ removed and what the runtime refused. *In progress, 2026-10-08.* The decision is recorded; the code is being built in the controller and in the store, forge and container runtime modules. The live dry runs are recorded here once the tools run. + +The first readings, 2026-10-08, before ADR 0257 was rolled out: + +- **The store** held 25.7 GiB. 469 manifests marking 23.8 GiB were unrecorded. Of those, 374 manifests in + 27 repositories were copied bases ([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md)), + and most of the rest were images pushed before the mesh existed + ([research 035](../../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)). +- **The package registry** held one package with four versions, all kept. Every lockfile on the forge + went unread, and every version read zero bytes ([issue 322](../../04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md)). diff --git a/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md new file mode 100644 index 00000000..7a64a20e --- /dev/null +++ b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md @@ -0,0 +1,36 @@ +--- +status: located +opened: 2026-10-08 +located-in: [mesh-controller internal/builder (standingOn, the copy of a base), internal/inventory (the keep set), internal/artifacts (LetGo), cmd/mesh-controller (the sweep); mesh-catalog modules/artifact-store-tools (classification)] +fixed-by: +amended-design: 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +--- + +# 321. A copied base was recorded by nothing, and a let-go index kept its platforms + +## Symptom + +On 2026-10-08 at 13:35, `artifact_store_references` (the store's tools of +[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)) +read the artifact store at 25.7 GiB. 469 manifests marking 23.8 GiB were **unrecorded**: no controller +record names them, so no sweep will ever let them go +([ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) §3). + +374 of those manifests are in 27 repositories named `/on-`. Only the builder writes +such a name: before a build, it copies there the image published elsewhere that the module's build +stands on ([ADR 0097](../../02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md)). The largest +is a route proxy's Go toolchain copy: 34 manifests, 10.4 GiB, all unrecorded. + +The same reading shows a second case. The repository of the route proxy's upstream artifact (a mirrored +`upstream` image, kind `upstream`) has one kept manifest of 23 KiB and 16 unrecorded manifests marking +30 MB. + +## Why it is a design issue + +ADR 0189 rests on one sentence: the mesh "has never put anything there it did not record". For copied +bases that sentence is false, and nothing said so until the store's tools counted it. Each copy is a +thing the mesh put in its store, and the records do not show it as made. So the sweep can never let a +copy go, however old it is, and the store only grows. The second case is the same promise broken +another way: letting an index go was recorded as collecting it, and the store kept every byte it named. + +The diagnosis is in [01-diagnosis.md](01-diagnosis.md). diff --git a/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md new file mode 100644 index 00000000..80d9149d --- /dev/null +++ b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md @@ -0,0 +1,55 @@ +# 321 — Diagnosis + +## 2026-10-08 + +**Where a copy is made.** The builder resolves a module's declared bases before it builds anything. +For an image published elsewhere, it copies the image into the store under +`/on-` and hands the recipe the copy. It also returns the copy as one of +the references the build stood on. The controller keeps those references in the build's +`built_against`. It keeps the build's artifacts in `made`. + +**What the sweep reads.** The keep set and the list of everything the mesh made are both read from +`made`. `built_against` is read only for build edges. So a copy is in the records, but not where the +sweep looks, and it is never counted as something the mesh made. + +**A failed build.** The controller records a failed build with no module and no `built_against`. A +build that copied its base and then failed, for example on a recipe refused for an undeclared fetch, +leaves its copy in the store with no record at all. Reading the records on 2026-10-08: + +- 23 distinct copies are named in `built_against` of successful builds. +- 17 of them were stood on by one of the five most recent builds of a module the controller holds. +- Several `on-` repositories in the store are named by no build at all. Examples: three copies of an + invoicing app's bases, from builds whose rows have no `built_against`; a module's Node base; and a + second version of the route proxy's toolchain image. + +**Why one per module.** The repository name is built from the module's name, so two modules standing +on one image hold two copies. The store keeps each blob once, so the copies cost manifests rather than +bytes. But the first copy into each repository asks the public registry for the whole image again. The +public registry rations anonymous pulls, and on 2026-09-28 it refused a whole catalogue's rebuild. + +**Why 10.4 GiB.** A copy is an image index with every platform the upstream publishes, Windows +included (ADR 0096 copies the index whole). The route proxy's repository holds two versions of a Go +toolchain image, each an index with 16 platform manifests. Four of those platform manifests are each +larger than 2 GB. I think those four are the Windows images, two per version, because the Linux +platforms are around 300 MB each; the index contents were not read to confirm it. + +**Why letting an index go frees nothing.** The copy puts each platform manifest under its own digest, +so each is a revision of the repository. The store's collector, run without deleting untagged +manifests, keeps every revision a repository holds. The sweep deletes the index's revision and nothing +else, so the platform manifests stay, still revisions, and every blob they name stays with them. The +store's tools show this in the route proxy's upstream-artifact repository: one kept index, and 16 +unrecorded platform manifests left behind by the indexes collected before it. + +**Ruled out.** + +- *The store's tools misclassify.* They classify a platform manifest on its own, which is also what the + store's collector does. So "unrecorded" is what collection actually sees. +- *Blob sharing makes the copies free.* It makes duplicate copies cheap. It does not make old versions + of an image free, and nothing ever lets them go. + +**Located** in the controller: the builder (where a copy is made and named), the inventory (what the +sweep reads), and the store client (what letting an index go deletes). The store's tools are located +there too, because they count a platform manifest apart from its index. + +**The fix** is [ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md) +and its two pull requests (the controller and the catalogue's store tools). diff --git a/04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md b/04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md new file mode 100644 index 00000000..d67b15a1 --- /dev/null +++ b/04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md @@ -0,0 +1,53 @@ +--- +status: located +opened: 2026-10-08 +located-in: [mesh-catalog modules/gitea (the npm-registry bundle: refs.go, retention.go)] +fixed-by: +amended-design: 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +--- + +# 322. Package retention read no lockfile, and said zero bytes for every kept version + +## Symptom + +On 2026-10-08 at 13:45, the forge's `npm_retention` was run as a dry run, with these results: + +- It read 50 repositories. +- It found one package with four versions and kept all four. +- It listed 32 `package-lock.json` files under `files_not_read`, each with the same error: + `json: cannot unmarshal string into Go struct field npmLockEntry.packages.dependencies of type main.npmLockEntry`. +- Every version read 0 bytes. The forge's own file sizes for the same versions, read through + `npm_packages`, are 20 954 to 28 426 bytes. + +## Why it is a design issue + +[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +§4 keeps a version when "a lockfile on the default branch of any repository on the forge names it". No +lockfile was read, so that rule kept nothing. Two further faults were silent: + +- **A size that was never measured read as zero.** The dry run asked the forge for the size of a version + only when it would delete that version. Every kept version therefore answered 0 bytes, with no word + that it had not been measured. +- **An unread file did not stop a real run.** Only an unread *repository* did. A real run would have + deleted a version that only an unread lockfile named, while its answer listed that file as unread. + +## Diagnosis + +1. **The reader knew one lockfile shape.** A lockfile of version 2 or 3 has a `packages` map from paths + to entries. In an entry, `dependencies` (like `devDependencies`, `optionalDependencies` and + `peerDependencies`) is a map from names to range strings. The reader decoded every entry with the + type of a version 1 entry, whose `dependencies` is a map of nested entries. One string failed the + whole file. +2. **Sizes were read only for the versions to delete.** This was an optimisation, and it made a kept + version's size indistinguishable from an empty one. +3. **The refusal checked repositories, not files.** + +**The fix** is in the forge module's Go bundle: + +- It reads versions 1, 2 and 3 one entry at a time. The package name comes from the path after the last + `node_modules/`, or from an alias's `name`. Workspace links are skipped. +- An entry of an unknown shape is named, and the rest of the file is still read. +- Every version is measured, and a size that could not be read is said. +- A file not read whole stops a real run, as an unread repository does. + +Tests use real lockfile shapes of all three versions, and a fake forge that serves file sizes. diff --git a/04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md b/04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md new file mode 100644 index 00000000..c2e4e06d --- /dev/null +++ b/04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md @@ -0,0 +1,38 @@ +--- +status: open +opened: 2026-10-08 +located-in: [] +fixed-by: +amended-design: +--- + +# 323. An image a definition pins under an older name of the store reads as unrecorded + +## Symptom + +On 2026-10-08, `artifact_store_references` classified three manifests as **unrecorded**: "no record names +it". The anchor runs all three in containers today. They are an invoicing app's API and web front, and a +static company website. Each is pinned by digest in a catalogue module's definition, under an older name +of the same artifact store. The anchor's declaration names them, and its containers run them; both were +matched by digest ([research 035](../../01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md)). + +The same word, *unrecorded*, describes 35 repositories and 2 manifests that nothing runs, declares or pins. + +## Why it is a design issue + +[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +§2 lists the states a manifest is in. *Kept — a definition names it* is read only for what the mesh made. +An image a definition names, which the mesh did not make, falls through to *unrecorded*. So the answer +that should tell a person "a machine runs this" tells them the same as "nothing knows this". + +Nothing removes an unrecorded manifest today (ADR 0189 §3), so no running image is at risk now. The risk +comes later. Research 035 asks how the images nothing uses may be let go of. A rule that reads +*unrecorded* as *unused* would take three running images with it, and nothing in the answer would warn +the person who runs it. + +## Questions to answer + +- Should the store's tools read the definitions, under every name the store has had, and say *named by a + definition* for a manifest the mesh did not make? +- Or should the three modules be moved to images the mesh builds, so that the case no longer exists, and + the catalogue's module check refuse a definition that pins an image under an older name of the store? From b6690c38cf3b7b93e86db01b4d086800956ce334 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 14:53:04 +0200 Subject: [PATCH 2/3] ADR 0257: platforms go only in a confirmed collect; say what stays, and what a rollback costs The sweep after a build is real collection with no dry run in front of it, so it lets an index go alone (review of #207). --- ...hat-stood-on-it-and-held-once-per-image.md | 56 +++++++++++++------ .../51-the-registries-keep-what-is-named.md | 16 ++++-- .../01-diagnosis.md | 7 +++ 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md index f11f461a..cea384a6 100644 --- a/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md +++ b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md @@ -78,10 +78,13 @@ Windows included. 1. *Let the index go alone.* Rejected for fact 3. 2. *Let the index go with every platform manifest it names.* Rejected, because a kept index of the same repository may name one of them. -3. **Let the index go with the platform manifests it names that no kept index of the repository also - names, platforms first.** Chosen. The controller reads only its records, so it cannot see an - unrecorded index that also names a platform. The store tools can see that, and their dry run lists - every manifest that stays and would lose a platform, before anybody runs collection for real. +3. *The same, in every sweep.* Rejected. The sweep after each build is real collection with no dry run + in front of it. The controller reads only its records, so it cannot see an unrecorded index, or a copy + a build is making at that moment, that names the same platform. +4. **Let the index go with the platform manifests it names that no kept index of the repository also + names, platforms first, only in a collect a person confirmed.** Chosen. The store tools can see every + manifest in a repository, and their dry run lists each one that stays and would lose a platform. A + person reads that before confirming. The sweep after a build lets an index go alone, as it did before. **The copies no record names**, made before this record by a failed build, or before builds kept what they stood on. @@ -93,7 +96,7 @@ they stood on. tools list each unrecorded copy at the top of a copy's repository, with what it alone marks. A new controller verb, `mirrors`, records the ones a person names, with a reason. It refuses any reference outside a repository only the copy step writes, and any reference the store does not hold. Recording - deletes nothing. + deletes nothing itself; a recorded copy then goes at the next sweep unless a kept build stood on it. **Copying only the platforms the mesh runs** was weighed and is left for a later record. It would remove most of the route proxy's toolchain copy, which is Windows images, but the reference a build is handed @@ -105,7 +108,9 @@ partly absent depends on its version and settings, and that has not been measure **1. One repository per upstream image.** A copied base is put in `upstream//` (the public hub is `docker.io`, and a port's colon becomes a dash). The module's own repository from before is offered as a source: where it holds the image by digest, the copy is made from it, and nothing is asked -of upstream. A module's build arguments name the new copy. Its source fingerprint is unchanged, because +of upstream. A copy counts as held only when its index and every platform manifest the index names are +held; otherwise it is copied again, which puts back what is missing. A module's build arguments name the +new copy. Its source fingerprint is unchanged, because the fingerprint reads a base by digest, never by where it was copied to. **2. A copy is recorded where it is copied.** The builder says, in every build outcome, what it copied. @@ -117,19 +122,23 @@ named it. Only the copy step ever wrote a repository so named. **3. A copy is kept by the kept builds that stood on it, and by nothing else.** A copy is kept, with the reason `stood-on`, while one of the five most recent successful builds of a module the mesh holds stood on it. Otherwise it is eligible, and the sweep lets it go as it lets any eligible artifact go. A -definition never keeps a copy. A copy already let go of stays let go of. The reason `stood-on` belongs to +definition never keeps a copy. A copy already let go of stays let go of, until a build copies it again: +the build then holds it, and its record of being let go of is removed. The reason `stood-on` belongs to copies alone: another module's artifact is still kept by that module's own builds. -**4. An index goes with its platforms.** When the sweep lets an index go, it first deletes the platform -manifests the index names that no kept index of the same repository names, and deletes the index last. A -sweep cut short leaves the index to be asked about again. If the sweep cannot read which platforms the -kept indexes name, it lets nothing go. This applies to every index the mesh let go of, upstream artifacts -included. +**4. An index goes with its platforms only in a collect a person confirmed.** Before its first delete, +a confirmed `collect` reads, for every repository it will touch, which platform manifests the kept +indexes there name. If any of those reads fails, it lets nothing go. Then, for each index it lets go of, +it first deletes the platform manifests that no kept index of the same repository names, and deletes the +index last. A collect cut short leaves the index to be asked about again. The sweep after a build lets an +index go alone. This applies to every index the mesh lets go of, upstream artifacts included. **5. A person may record a copy that no record names.** The controller's `mirrors` verb lists the copies the records hold, with their states. Given references, it records the ones that are in a repository only the copy step writes and that the store holds. It is a dry run without `confirm`. With `confirm` it needs -a `why`, and it is recorded as a hand act. It deletes nothing. `artifact_store_references` lists the +a `why`, and it is recorded as a hand act once the copies are recorded. It deletes nothing itself, but a +recorded copy is eligible, and the next sweep lets it go unless a kept build stood on it; the dry run says +so before anything is recorded. `artifact_store_references` lists the candidates in `unrecorded_mirrors`, largest first. **6. The store tools count a platform as its index's.** A platform manifest takes the state of the @@ -153,6 +162,11 @@ ADR 0097 is superseded in one sentence only: a base is copied into the image's o - Most of the 23.8 GiB is copies no record names, and pre-mesh images (research [035](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)). Those wait for a person, through `mirrors` and, for the pre-mesh images, a decision of its own. +- **Rolling the controller back is not free once old copies are gone.** After this is rolled out, the + modules' former `/on-` copies stop being stood on and are let go of. A controller + rolled back to the version before this record names those repositories again. It finds them empty and + copies every base from the public registry again, which rations anonymous pulls. A rollback after that + point should be planned around the pull limit. - The move to one repository per image costs one copy per image, taken from the store itself. After it, each module's former copies stop being stood on, become eligible after five more builds of that module, and go, freeing their manifests. @@ -169,7 +183,10 @@ ADR 0097 is superseded in one sentence only: a base is copied into the image's o - **The builder.** - The repository is named for the image: the hub, other hosts, a port, upper case. - A copy held under the module's former repository is mounted, every blob, with nothing uploaded - again and nothing asked of an upstream that is gone. + again and nothing asked of an upstream that is gone. A store that answers a mount with an upload + gets each blob uploaded. + - An index missing a platform, in the image's repository or the former one, is not taken as held. + - Builds copying while platforms are let go of under them end with the copy whole. - A second module standing on the image copies nothing. - A failed build and a working build both say what they copied. - **The records.** @@ -177,16 +194,19 @@ ADR 0097 is superseded in one sentence only: a base is copied into the image's o - A copy is kept, with `stood-on`, by the five kept builds and by nothing else, and a definition naming the image keeps no copy. - Another module's artifact is not kept as a copy. - - A collected copy stays collected. + - A collected copy stays collected, and a copy a build copies again is held again; a person's record + never brings a collected copy back. - A person's record stands over a later build's. - The migration's backfill records one copy, by its earliest build, and records nothing outside an `on-` repository. - A copy's repository draws no build edge. - **The sweep.** - - Against a fake store, an index goes with its own platforms, platforms first. - - A platform a kept index names stays. + - Through the records, a confirmed collect lets an eligible index go with its own platform, platforms + first, and leaves the platform a kept index of the same repository names. + - The sweep after a build lets an index go alone. - An image is one delete. - - A store that cannot say what a kept index names keeps the index. + - A kept index the store will not answer for stops a confirmed collect before its first delete, and an + index in a repository not read beforehand is kept. - **The `mirrors` verb.** - It is composed only with what it takes. - It takes only references in a copy's repository. diff --git a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md index 66a90111..b91ea5c7 100644 --- a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +++ b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md @@ -111,8 +111,12 @@ A build that stands on an image published elsewhere copies it into the store fir build records already named were recorded by the migration that made the table. - **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. -- **An index goes with its platforms.** The sweep deletes the platform manifests an index names that - no kept index of its repository also names, and deletes the index last. +- **Held whole.** A copy counts as held only when its index and every platform the index names are + held; otherwise it is copied again. A copy a build copies again is no longer recorded as let go of. +- **An index goes with its platforms only in a confirmed collect.** Before its first delete, it reads + which platforms the kept indexes of every repository it touches name, and lets nothing go if a read + fails. It deletes the platform manifests an index names that no kept index of its repository also + names, and deletes the index last. The sweep after a build lets an index go alone. - **A copy no record names** waits for a person. `artifact_store_references` lists each one in `unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a person names, and the sweep then decides about them. @@ -125,11 +129,13 @@ A build that stands on an image published elsewhere copies it into the store fir - **`collect`** says what the sweep would let go of. With `confirm` and a `why` it runs the sweep that runs after a build: every kept archive held first, then each eligible reference let go and recorded as collected, stopping at the first refusal by the store. It is bounded by a count and a time, says - what is left, and is a hand-act recorded with its `why`. An index is let go of with its own platform - manifests (ADR 0257). + what is left, and is a hand-act recorded with its `why`. A confirmed collect lets an index go with its + own platform manifests (ADR 0257); the sweep after a build lets it go alone. - **`mirrors`** lists the copied bases the records hold, with their states. Given references, it records the copies that are in a repository only the copy step writes and that the store holds. It is a dry - run without `confirm`; with `confirm` it needs a `why`, and it is a hand act. It deletes nothing. + run without `confirm`; with `confirm` it needs a `why`, and it is a hand act, logged once the copies + are recorded. It deletes nothing itself, but a recorded copy is eligible: the next sweep lets it go + unless a kept build stood on it, and both the dry run and the real run say so. `artifact_store_collect` counts the platforms that go with an index, and its dry run lists, in `platforms_named_by_what_stays`, every manifest that stays and names a platform that would go. The diff --git a/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md index 80d9149d..1d6f2100 100644 --- a/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md +++ b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md @@ -51,5 +51,12 @@ unrecorded platform manifests left behind by the indexes collected before it. sweep reads), and the store client (what letting an index go deletes). The store's tools are located there too, because they count a platform manifest apart from its index. +**What the fix leaves.** The 16 platform manifests in the route proxy's upstream-artifact repository +stay. The indexes that named them were let go of before this fix, so no record names them, and neither +the sweep nor a confirmed collect will touch them. They can be removed in one of two ways, each a +person's act. One is the `mirrors` verb's way of recording them, if the decision is extended to the +repository of an upstream artifact. The other is a decision on how the store lets go of manifests no +record names (research 035 asks the same question for the pre-mesh images). + **The fix** is [ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md) and its two pull requests (the controller and the catalogue's store tools). From 56e78612dcac923bfbaa1a1a0c806ed9554dbc7f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 15:50:36 +0200 Subject: [PATCH 3/3] ADR 0257: the sweep after a build leaves an index for a confirmed collect Let go of alone, an index's platforms would stay for ever under a collected record (re-review of #207). --- ...ilds-that-stood-on-it-and-held-once-per-image.md | 13 +++++++++---- .../51-the-registries-keep-what-is-named.md | 5 +++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md index cea384a6..7a082ab2 100644 --- a/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md +++ b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md @@ -84,7 +84,9 @@ Windows included. 4. **Let the index go with the platform manifests it names that no kept index of the repository also names, platforms first, only in a collect a person confirmed.** Chosen. The store tools can see every manifest in a repository, and their dry run lists each one that stays and would lose a platform. A - person reads that before confirming. The sweep after a build lets an index go alone, as it did before. + person reads that before confirming. The sweep after a build does not touch an index: it leaves it + eligible for that collect. Letting it go alone would be option 1, and would record it as collected, + so no later collect would offer it again and its platforms would stay for ever. **The copies no record names**, made before this record by a failed build, or before builds kept what they stood on. @@ -130,8 +132,10 @@ copies alone: another module's artifact is still kept by that module's own build a confirmed `collect` reads, for every repository it will touch, which platform manifests the kept indexes there name. If any of those reads fails, it lets nothing go. Then, for each index it lets go of, it first deletes the platform manifests that no kept index of the same repository names, and deletes the -index last. A collect cut short leaves the index to be asked about again. The sweep after a build lets an -index go alone. This applies to every index the mesh lets go of, upstream artifacts included. +index last. A collect cut short leaves the index to be asked about again. The sweep after a build asks +the store whether each eligible reference is an index (one read of its manifest). It skips every index, +leaving it eligible and not recorded as collected, and lets go of everything else as before; a read that +fails stops the sweep. This applies to every index the mesh lets go of, upstream artifacts included. **5. A person may record a copy that no record names.** The controller's `mirrors` verb lists the copies the records hold, with their states. Given references, it records the ones that are in a repository only @@ -203,7 +207,8 @@ ADR 0097 is superseded in one sentence only: a base is copied into the image's o - **The sweep.** - Through the records, a confirmed collect lets an eligible index go with its own platform, platforms first, and leaves the platform a kept index of the same repository names. - - The sweep after a build lets an index go alone. + - The sweep after a build leaves an eligible index eligible and lets an image go, and a later confirmed + collect takes the index with its own platform. - An image is one delete. - A kept index the store will not answer for stops a confirmed collect before its first delete, and an index in a repository not read beforehand is kept. diff --git a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md index b91ea5c7..03611752 100644 --- a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +++ b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md @@ -116,7 +116,8 @@ A build that stands on an image published elsewhere copies it into the store fir - **An index goes with its platforms only in a confirmed collect.** Before its first delete, it reads which platforms the kept indexes of every repository it touches name, and lets nothing go if a read fails. It deletes the platform manifests an index names that no kept index of its repository also - names, and deletes the index last. The sweep after a build lets an index go alone. + names, and deletes the index last. The sweep after a build skips every eligible index and leaves it + eligible for such a collect; it lets other eligible artifacts go as before. - **A copy no record names** waits for a person. `artifact_store_references` lists each one in `unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a person names, and the sweep then decides about them. @@ -130,7 +131,7 @@ A build that stands on an image published elsewhere copies it into the store fir runs after a build: every kept archive held first, then each eligible reference let go and recorded as collected, stopping at the first refusal by the store. It is bounded by a count and a time, says what is left, and is a hand-act recorded with its `why`. A confirmed collect lets an index go with its - own platform manifests (ADR 0257); the sweep after a build lets it go alone. + own platform manifests (ADR 0257); the sweep after a build leaves an eligible index for it. - **`mirrors`** lists the copied bases the records hold, with their states. Given references, it records the copies that are in a repository only the copy step writes and that the store holds. It is a dry run without `confirm`; with `confirm` it needs a `why`, and it is a hand act, logged once the copies