Indexer (A-326): refuse at take-in a cycle with a build edge no build seat's running version breaks
The take-in computes the revision's catalog graph over its dry run and refuses one whose edges hold a cycle with a build edge (kind 1 or 3), naming every artifact on it, as a refusal by its validation is said; a kind-3 edge out of a module holding a build seat into an artifact that seat builds breaks the cycle at the running version. Builds: design 47 G §49, §51, §270; A-326 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -378,3 +378,82 @@ func (ix *Indexer) withActs(ctx context.Context, entries map[string]meshcatalog.
|
||||
}
|
||||
return WithActs(entries, defs)
|
||||
}
|
||||
|
||||
// RefusedCycle is a cycle of the entries' edges that the take-in refuses (design 47 G §49, A-326), as the artifacts it passes
|
||||
// through with the first named again at its end; nil when there is none. A cycle of delivery edges (kinds 2, 4, 5, 6) alone
|
||||
// is delivered as one step, so only a cycle holding a build edge (kind 1 or 3) is refused. A build edge of kind 3 that goes
|
||||
// out of an artifact of a module holding a build seat into an artifact that seat builds is where such a cycle is broken: the
|
||||
// running version builds the new one (§270), so a cycle through it is allowed, as the cycle through a build module's own
|
||||
// inputs is.
|
||||
func RefusedCycle(entries map[string]meshcatalog.CatalogEntry) []meshcatalog.ArtifactRef {
|
||||
var edges []meshcatalog.Edge
|
||||
for _, m := range slices.Sorted(maps.Keys(entries)) {
|
||||
for _, x := range entries[m].Edges {
|
||||
if x.From != "" && x.To != "" && !brokenAtTheRunningVersion(x, entries) {
|
||||
edges = append(edges, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
next := map[meshcatalog.ArtifactRef][]meshcatalog.ArtifactRef{}
|
||||
for _, x := range edges {
|
||||
if !slices.Contains(next[x.From], x.To) {
|
||||
next[x.From] = append(next[x.From], x.To)
|
||||
}
|
||||
}
|
||||
for _, x := range edges {
|
||||
if x.Kind != 1 && x.Kind != 3 {
|
||||
continue
|
||||
}
|
||||
if path := shortestPath(next, x.To, x.From); path != nil {
|
||||
return append([]meshcatalog.ArtifactRef{x.From}, path...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// brokenAtTheRunningVersion says whether x is a build edge of kind 3 out of an artifact of a module that holds a build seat
|
||||
// (an acting rule of step build) into an artifact that seat builds (A-326).
|
||||
func brokenAtTheRunningVersion(x meshcatalog.Edge, entries map[string]meshcatalog.CatalogEntry) bool {
|
||||
if x.Kind != 3 {
|
||||
return false
|
||||
}
|
||||
builder, to := entries[strings.SplitN(x.From, ":", 2)[0]], entries[strings.SplitN(x.To, ":", 2)[0]]
|
||||
i := slices.IndexFunc(to.Artifacts, func(a meshcatalog.Artifact) bool { return a.Ref == x.To })
|
||||
if i < 0 {
|
||||
return false
|
||||
}
|
||||
art := to.Artifacts[i]
|
||||
return slices.ContainsFunc(builder.Acts, func(r meshcatalog.ActingRule) bool {
|
||||
if r.Step != seatdef.StepBuild {
|
||||
return false
|
||||
}
|
||||
if r.On == seatdef.OnKindImage {
|
||||
return art.Kind == "image"
|
||||
}
|
||||
return seatdef.ActingRule{On: r.On}.ActsOn(art.Language)
|
||||
})
|
||||
}
|
||||
|
||||
// shortestPath is the shortest path of next from start to end, start and end included; nil when end cannot be reached.
|
||||
func shortestPath(next map[meshcatalog.ArtifactRef][]meshcatalog.ArtifactRef, start, end meshcatalog.ArtifactRef) []meshcatalog.ArtifactRef {
|
||||
before := map[meshcatalog.ArtifactRef]meshcatalog.ArtifactRef{start: ""}
|
||||
queue := []meshcatalog.ArtifactRef{start}
|
||||
for len(queue) > 0 {
|
||||
at := queue[0]
|
||||
queue = queue[1:]
|
||||
if at == end {
|
||||
var path []meshcatalog.ArtifactRef
|
||||
for ; at != ""; at = before[at] {
|
||||
path = append([]meshcatalog.ArtifactRef{at}, path...)
|
||||
}
|
||||
return path
|
||||
}
|
||||
for _, n := range next[at] {
|
||||
if _, seen := before[n]; !seen {
|
||||
before[n] = at
|
||||
queue = append(queue, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -227,3 +227,41 @@ func TestAKeptRevisionHoldsTheActsAndTheKindFiveEdges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A-326 (design 47 G §49, §51): a cycle through a build module's own inputs is broken at its running version and allowed; any
|
||||
// other cycle with a build edge is refused, naming every artifact on it; a cycle of delivery edges alone is one step.
|
||||
func TestACycleWithABuildEdgeIsRefusedUnlessABuildSeatsRunningVersionBreaksIt(t *testing.T) {
|
||||
builder := meshcatalog.CatalogEntry{Module: "mesh-build-go", Artifacts: []meshcatalog.Artifact{{Ref: "mesh-build-go:files", Language: "go"}},
|
||||
Acts: []meshcatalog.ActingRule{{Seat: "node-build-go", Step: seatdef.StepBuild, On: "go"}}}
|
||||
handActs := meshcatalog.CatalogEntry{Module: "mesh-hand-acts", Artifacts: []meshcatalog.Artifact{{Ref: "mesh-hand-acts:tools", Language: "go"}},
|
||||
Edges: []meshcatalog.Edge{{Kind: 3, From: "mesh-build-go:files", To: "mesh-hand-acts:tools", Word: "toolchains", Why: "go"}}}
|
||||
trunkCycle := builder
|
||||
trunkCycle.Edges = []meshcatalog.Edge{{Kind: 6, From: "mesh-hand-acts:tools", To: "mesh-build-go:files", Word: "seat:mesh-hand-acts.hand-act"}}
|
||||
a := meshcatalog.CatalogEntry{Module: "a", Artifacts: []meshcatalog.Artifact{{Ref: "a:go", Language: "go"}},
|
||||
Edges: []meshcatalog.Edge{{Kind: 1, From: "b:go", To: "a:go", Word: "packages"}}}
|
||||
b := meshcatalog.CatalogEntry{Module: "b", Artifacts: []meshcatalog.Artifact{{Ref: "b:go", Language: "go"}},
|
||||
Edges: []meshcatalog.Edge{{Kind: 1, From: "a:go", To: "b:go", Word: "packages"}}}
|
||||
noSeat := trunkCycle
|
||||
noSeat.Acts = nil
|
||||
verbsOnly := meshcatalog.CatalogEntry{Module: "c", Artifacts: []meshcatalog.Artifact{{Ref: "c:go"}}, Edges: []meshcatalog.Edge{{Kind: 6, From: "d:go", To: "c:go"}}}
|
||||
verbsBack := meshcatalog.CatalogEntry{Module: "d", Artifacts: []meshcatalog.Artifact{{Ref: "d:go"}}, Edges: []meshcatalog.Edge{{Kind: 4, From: "c:go", To: "d:go"}}}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
entries []meshcatalog.CatalogEntry
|
||||
want []meshcatalog.ArtifactRef
|
||||
}{
|
||||
{"the trunk catalogue's cycle through a build module's own inputs", []meshcatalog.CatalogEntry{trunkCycle, handActs}, nil},
|
||||
{"two artifacts each needing the other's new package", []meshcatalog.CatalogEntry{a, b}, []meshcatalog.ArtifactRef{"b:go", "a:go", "b:go"}},
|
||||
{"the same cycle from a module that holds no build seat", []meshcatalog.CatalogEntry{noSeat, handActs},
|
||||
[]meshcatalog.ArtifactRef{"mesh-build-go:files", "mesh-hand-acts:tools", "mesh-build-go:files"}},
|
||||
{"a cycle of delivery edges alone", []meshcatalog.CatalogEntry{verbsOnly, verbsBack}, nil},
|
||||
} {
|
||||
entries := map[string]meshcatalog.CatalogEntry{}
|
||||
for _, e := range c.entries {
|
||||
entries[e.Module] = e
|
||||
}
|
||||
if got := RefusedCycle(entries); !slices.Equal(got, c.want) {
|
||||
t.Errorf("%s: refused %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,6 +792,9 @@ func (ix *Indexer) evaluate(ctx context.Context, own string, start bool) (Applie
|
||||
if err != nil {
|
||||
return Applied{}, "", err
|
||||
}
|
||||
if words, refused, err := ix.refuseCycle(ctx, ready, dry); refused || err != nil {
|
||||
return Applied{}, words, err
|
||||
}
|
||||
if names == own {
|
||||
a, err := ix.takeInWhole(ctx, ready, own, "")
|
||||
if errors.Is(err, errClaimLost) {
|
||||
@@ -809,6 +812,34 @@ func (ix *Indexer) evaluate(ctx context.Context, own string, start bool) (Applie
|
||||
return Applied{}, ix.waitingLine(w), ix.holdFor(ctx, ready, names, own, dry)
|
||||
}
|
||||
|
||||
// refuseCycle refuses the revision whose catalog graph would hold a cycle with a build edge the take-in refuses (design 47 G
|
||||
// §49, A-326; RefusedCycle), over the entries the dry run computed: written as the refusal of its newest value and said in
|
||||
// take-in.pending with every artifact on the cycle, once, as a refusal by its validation is (§713). It answers whether it refused.
|
||||
func (ix *Indexer) refuseCycle(ctx context.Context, ready []PendingEntry, d *dryRun) (string, bool, error) {
|
||||
entries, err := d.ix.viewEntries(ctx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
cycle := RefusedCycle(WithEdges(ix.withActs(ctx, entries)))
|
||||
if cycle == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
n := newestOfRevision(ready)
|
||||
refused := "a cycle with a build edge, which no running version breaks (design 47 G §49): " + strings.Join(cycle, ", ")
|
||||
words := fmt.Sprintf("the revision at %d is refused: %s", ready[n].Value.Seq, refused)
|
||||
t := revisionWait(takein.TakeInPending, ready, "")
|
||||
t.Refused = refused
|
||||
if e := ready[n].Value; e.Refused == refused && e.Said == t.Key && e.SaidKind == takein.TakeInPending {
|
||||
return words, true, nil
|
||||
}
|
||||
ix.logf("take-in: %s", words)
|
||||
if !ix.sayTakeIn(ctx, t) {
|
||||
return words, true, nil
|
||||
}
|
||||
ix.markSaid(ctx, ready[n:n+1], &t, func(p *Pending) { p.Refused = refused })
|
||||
return words, true, nil
|
||||
}
|
||||
|
||||
// holdFull marks the ready merges held with the reason objects full (Pending.Held), read again at their revisions now
|
||||
// (the take-in's claim moved them), so indexer_pending shows why they wait.
|
||||
func (ix *Indexer) holdFull(ctx context.Context, ready []PendingEntry) {
|
||||
|
||||
@@ -416,6 +416,31 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("A-326 a revision whose catalog graph holds a cycle with a build edge no build seat breaks is refused, naming it", func(t *testing.T) {
|
||||
// avahi's artifact needs the indexer's (kind 2), and the indexer is built on avahi (kind 3): avahi holds no build seat, so
|
||||
// no running version breaks the cycle, and the revision is refused at take-in naming every artifact on it (design 47 G §49).
|
||||
b := newTakeInBed(t)
|
||||
f := b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2")
|
||||
f.Manifests = map[string]string{}
|
||||
for dir, raw := range b.base.Manifests {
|
||||
f.Manifests[dir] = raw
|
||||
}
|
||||
f.Manifests["modules/avahi"] = strings.Replace(f.Manifests["modules/avahi"], `"name": "tools",`, `"name": "tools", "needs": ["`+testSelf+`:tools"],`, 1)
|
||||
f.Manifests["modules/"+testSelf] = strings.Replace(f.Manifests["modules/"+testSelf], `"build": {`,
|
||||
`"build": {
|
||||
"on": [{"arg": "RUNTIME_BASE", "module": "avahi", "artifact": "tools"}],`, 1)
|
||||
b.arrive(t, 2, f)
|
||||
want := "avahi:tools, " + testSelf + ":tools, avahi:tools"
|
||||
if len(b.said) == 0 || !strings.Contains(b.said[len(b.said)-1].Refused, want) {
|
||||
t.Fatalf("said %+v, want the revision refused naming %s", b.said, want)
|
||||
}
|
||||
if b.commit(t, "avahi") == sha('c') {
|
||||
t.Errorf("avahi was taken in at the refused revision")
|
||||
}
|
||||
if waits := PendingWaits(readPending(t, b)); len(waits) != 1 || !strings.Contains(waits[0].Refused, want) {
|
||||
t.Errorf("indexer_pending %+v, want the refusal naming the cycle", waits)
|
||||
}
|
||||
})
|
||||
t.Run("T10 the named indexer takes the revision in at its start; a gathered group stays", func(t *testing.T) {
|
||||
b := newTakeInBed(t)
|
||||
b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2"))
|
||||
|
||||
Reference in New Issue
Block a user