Hold every kept archive by a manifest so the store's collector keeps it (hq issue 253)
The store's garbage-collect marks only from manifests, and archives were published as bare blobs, so the first real collection would delete every archive the mesh keeps. PublishArchive now puts a deterministic OCI holder manifest (empty config, one layer) beside each archive; the sweep holds every kept archive before it lets anything go, which backfills existing bare blobs, and lets go of an archive holder-first. A forgotten module no longer keeps its five recent builds (ADR 0189). `collection [--json]` reports kept archives held/unheld and what may be let go, so the dry run can be lifted on evidence.
This commit is contained in:
@@ -3,6 +3,7 @@ package inventory
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
@@ -33,7 +34,8 @@ const KeptBuilds = 5
|
||||
// - **a definition names it** — the reference appears in a module's recorded manifest, which is
|
||||
// what the mesh would hand a machine now. No age limit: this is the floor;
|
||||
// - **the mesh can still go back to it** — it is an artifact of one of the KeptBuilds most
|
||||
// recent successful builds of its module;
|
||||
// recent successful builds of a module the mesh still holds. A forgotten module keeps
|
||||
// nothing beyond what a held definition names (novox/hq issue 253);
|
||||
// - it was already collected, in which case there is nothing left to do.
|
||||
//
|
||||
// Returned in a stated order so two runs over the same records ask for the same things in the
|
||||
@@ -109,12 +111,19 @@ func (i *Inventory) keptReferences(ctx context.Context) (map[string]bool, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The KeptBuilds most recent successful builds of each module, whole.
|
||||
// The KeptBuilds most recent successful builds of each module the mesh still holds, whole.
|
||||
//
|
||||
// **Only a module the mesh still holds can be gone back to** (novox/hq issue 253). "Somewhere
|
||||
// to return to" is a reason about a module's releases; a module that has been forgotten has
|
||||
// no releases left to return between, and its build rows stay only as history. Without the
|
||||
// join every module ever built kept five builds' artifacts for ever — and once the store's
|
||||
// collector runs for real, what the keep set says is what the disk holds.
|
||||
recent, err := i.store.Pool().Query(ctx,
|
||||
`select made from (
|
||||
select made, row_number() over (partition by module order by at desc, id desc) as back
|
||||
from build
|
||||
where failed = '' and module is not null and module <> ''
|
||||
select b.made, row_number() over (partition by b.module order by b.at desc, b.id desc) as back
|
||||
from build b
|
||||
join module m on m.name = b.module
|
||||
where b.failed = '' and b.module is not null and b.module <> ''
|
||||
) ranked where back <= $1`, KeptBuilds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -169,6 +178,28 @@ func (i *Inventory) keptReferences(ctx context.Context) (map[string]bool, error)
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
// KeptArchives is every archive the mesh keeps, in a stated order: the references the sweep must
|
||||
// hold by a manifest before it lets anything go, and the ones an operator needs to read as all
|
||||
// held before the store's collector is let loose (novox/hq issue 253, ADR 0189).
|
||||
//
|
||||
// Only references into the mesh's own store, and only blobs: an image is its own manifest, and a
|
||||
// reference that is kept because nothing here can speak for it is not one the store can be asked
|
||||
// about.
|
||||
func (i *Inventory) KeptArchives(ctx context.Context) ([]string, error) {
|
||||
keep, err := i.keptReferences(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for reference := range keep {
|
||||
if path, ours := catalogue.InArtifactStore(reference); ours && strings.Contains(path, "/blobs/sha256:") {
|
||||
out = append(out, reference)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// everyReferenceMade is every artifact reference any successful build recorded.
|
||||
func (i *Inventory) everyReferenceMade(ctx context.Context) ([]string, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
|
||||
@@ -20,6 +20,19 @@ func ref(module, artifact string, n int) string {
|
||||
return fmt.Sprintf("%s%s/%s@sha256:%064x", catalogue.ArtifactStoreScheme, module, artifact, n)
|
||||
}
|
||||
|
||||
// holding registers a definition for each module that names no artifact, so the mesh holds the
|
||||
// module and its recent builds are somewhere it can go back to — and nothing more.
|
||||
func holding(t *testing.T, inv *Inventory, modules ...string) {
|
||||
t.Helper()
|
||||
for _, module := range modules {
|
||||
m := catalogue.Manifest{Module: module, Version: "1"}
|
||||
if err := inv.RegisterModule(context.Background(), m,
|
||||
Source{Repository: "https://forge.invalid/" + module + ".git"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// built records one successful build of a module publishing one image.
|
||||
func built(t *testing.T, inv *Inventory, id, module string, n int) string {
|
||||
t.Helper()
|
||||
@@ -35,6 +48,7 @@ func built(t *testing.T, inv *Inventory, id, module string, n int) string {
|
||||
func TestTheStoreKeepsTheRecentBuildsAndLetsGoOfTheRest(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
holding(t, inv, "web")
|
||||
|
||||
// Eight builds of one module, oldest first. Five are kept — the newest, and the four a
|
||||
// release that turns out wrong can be taken back to.
|
||||
@@ -98,6 +112,7 @@ func TestWhatHasBeenCollectedIsNotOfferedAgain(t *testing.T) {
|
||||
// time it runs, for ever — a number of requests that grows with the mesh's whole history.
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
holding(t, inv, "web")
|
||||
for i := 1; i <= 7; i++ {
|
||||
built(t, inv, fmt.Sprintf("b%02d", i), "web", i)
|
||||
}
|
||||
@@ -123,6 +138,7 @@ func TestWhatHasBeenCollectedIsNotOfferedAgain(t *testing.T) {
|
||||
func TestAFailedBuildNamesNothingToCollectAndEachModuleIsCountedOnItsOwn(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
holding(t, inv, "web", "db")
|
||||
|
||||
// A failed build published nothing, so it is neither kept nor collected — and it must not
|
||||
// count against the module's five.
|
||||
@@ -156,6 +172,7 @@ func TestAFailedBuildNamesNothingToCollectAndEachModuleIsCountedOnItsOwn(t *test
|
||||
func TestAnArtifactRecordedWithAnAddressIsOfferedAsTheMeshRecordsOne(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
holding(t, inv, "tools")
|
||||
|
||||
// The oldest build published the old way; five newer ones fill the module's five.
|
||||
old := aBuild("a00", "tools", "")
|
||||
@@ -190,3 +207,82 @@ func TestAnArtifactRecordedWithAnAddressIsOfferedAsTheMeshRecordsOne(t *testing.
|
||||
t.Fatalf("offered %v again after collecting it", again)
|
||||
}
|
||||
}
|
||||
|
||||
// A forgotten module keeps nothing beyond what a held definition names (novox/hq issue 253).
|
||||
//
|
||||
// "Somewhere to go back to" is a reason about a module's releases, and a module the mesh no
|
||||
// longer holds has none. Its build rows stay as history; its artifacts go — except one a module
|
||||
// the mesh still holds names, which is the floor whatever built it.
|
||||
func TestAForgottenModuleKeepsNothingAHeldDefinitionDoesNotName(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Three builds of a module that was never held, or was held and then forgotten: within its
|
||||
// five, and kept for that reason until now.
|
||||
var gone []string
|
||||
for i := 1; i <= 3; i++ {
|
||||
gone = append(gone, built(t, inv, fmt.Sprintf("o%02d", i), "old", 200+i))
|
||||
}
|
||||
// A module the mesh holds, whose definition runs the forgotten module's newest image.
|
||||
named := gone[2]
|
||||
m := catalogue.Manifest{Module: "web", Version: "1", Resources: []map[string]any{{
|
||||
"id": "app", "type": "container", "name": "web", "image": named,
|
||||
}}}
|
||||
if err := inv.RegisterModule(ctx, m, Source{Repository: "https://forge.invalid/web.git"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go_, err := inv.ToCollect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(go_) != 2 || go_[0] != gone[0] || go_[1] != gone[1] {
|
||||
t.Fatalf("offered %v; want %v — a forgotten module's builds are no release to go back to, "+
|
||||
"and only what a held definition names stays", go_, gone[:2])
|
||||
}
|
||||
|
||||
// And once the module is held again, its five are kept again.
|
||||
holding(t, inv, "old")
|
||||
again, err := inv.ToCollect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(again) != 0 {
|
||||
t.Fatalf("offered %v for a module the mesh holds, within its five", again)
|
||||
}
|
||||
}
|
||||
|
||||
// The archives the mesh keeps are what the sweep holds before it lets anything go, and what an
|
||||
// operator reads as all held before the store's collector is let loose (novox/hq issue 253).
|
||||
func TestKeptArchivesAreTheKeptBlobsOnly(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := context.Background()
|
||||
holding(t, inv, "shell")
|
||||
|
||||
archive := func(n int) string {
|
||||
return fmt.Sprintf("%sshell/config/blobs/sha256:%064x", catalogue.ArtifactStoreScheme, n)
|
||||
}
|
||||
for i := 1; i <= 6; i++ {
|
||||
b := aBuild(fmt.Sprintf("s%02d", i), "shell", "")
|
||||
b.Made = []Artifact{
|
||||
{Name: "app", Kind: "image", Reference: ref("shell", "app", i)},
|
||||
{Name: "config", Kind: "archive", Reference: archive(i)},
|
||||
}
|
||||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
kept, err := inv.KeptArchives(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{archive(2), archive(3), archive(4), archive(5), archive(6)}
|
||||
if len(kept) != len(want) {
|
||||
t.Fatalf("kept archives %v; want the five recent ones and no images", kept)
|
||||
}
|
||||
for i := range want {
|
||||
if kept[i] != want[i] {
|
||||
t.Fatalf("kept archives %v; want %v", kept, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user