diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go index bfa522b0..e3b2e22b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go @@ -34,6 +34,8 @@ type graphKeeper struct { source int64 ok bool said string + // cycled is the cycles last said (assigngraph.Cycles). + cycled string // pins removes a revision that ceased to be current once nothing pins it (pins.go); nil keeps every revision. pins *pinKeeper // computed is the revision computed by the last pass, with its entries: the edges the wants wait on (actingwaits.go). @@ -60,6 +62,12 @@ func (p *passer) keepGraph(ctx context.Context) { p.logf("the assignment graph leaves out %s", u) } } + if cycles := strings.Join(c.Cycles, "; "); cycles != g.cycled { + g.cycled = cycles + if cycles != "" { + p.logf("the assignment graph holds a cycle among the waits within one step, and its entries wait for each other: %s", cycles) + } + } if g.has && g.ok && assigngraph.Same(g.last, c.Revision) { return } diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go index fca2b7fb..2b5b8750 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go @@ -48,6 +48,8 @@ type Computed struct { // Uncomputed is each placed module whose entry could not be computed, as "/: why", sorted: it is said, // never left out in silence. Uncomputed []string + // Cycles are the cycles among the waits within one step (Cycles, design 47 G §272): said, never left in silence. + Cycles []string } // placedAt is one placed module with what its entry is computed from. @@ -212,6 +214,7 @@ func Compute(in Inputs) Computed { rev.Entries = append(rev.Entries, meshwants.NodeEntryRef{Node: p.node, Module: p.placed.Module, Digest: d}) } out.Revision = rev + out.Cycles = Cycles(slices.Collect(maps.Values(out.Entries))) slices.Sort(out.Uncomputed) return out } diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/cycle.go b/modules/mesh-delivery-assigner/internal/assigngraph/cycle.go new file mode 100644 index 00000000..a8d691f5 --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assigngraph/cycle.go @@ -0,0 +1,73 @@ +package assigngraph + +import ( + "fmt" + "maps" + "slices" + "strings" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" +) + +// Cycles are the cycles among the waits within one step (design 47 G §272, ADR 0656): an entry waits for its acting parts in the step an +// edge names, so two acting parts acting on each other in one step wait for each other for ever. Each is `: a -> b -> a`, by +// node and module, sorted. Edges of two steps never close a cycle together: the indexer taking in the checker and the checker checking +// the indexer are none, and a holder's own inputs are no edge here (the graph gives an acting edge only from a part to the entries it +// acts on, never to what the part is built from). +func Cycles(entries []meshwants.AssignmentEntry) []string { + type key struct{ step, at string } + next := map[key][]string{} + steps := map[string]bool{} + for _, e := range entries { + for _, edge := range e.Edges { + if edge.Kind != EdgeActing { + continue + } + k := key{edge.Step, e.Node + "/" + e.Module} + next[k] = append(next[k], edge.Node+"/"+edge.Module) + steps[edge.Step] = true + } + } + var out []string + for _, step := range slices.Sorted(maps.Keys(steps)) { + const ( + open = iota + 1 + done + ) + state := map[string]int{} + var path []string + var walk func(at string) + walk = func(at string) { + state[at] = open + path = append(path, at) + for _, to := range next[key{step, at}] { + switch state[to] { + case 0: + walk(to) + case open: + from := slices.Index(path, to) + cycle := slices.Clone(path[from:]) + // start at the smallest member so one cycle is said once, whichever member is met first + lowest := slices.Index(cycle, slices.Min(cycle)) + cycle = append(cycle[lowest:], cycle[:lowest]...) + out = append(out, fmt.Sprintf("%s: %s -> %s", step, strings.Join(cycle, " -> "), cycle[0])) + } + } + path = path[:len(path)-1] + state[at] = done + } + starts := map[string]bool{} + for k := range next { + if k.step == step { + starts[k.at] = true + } + } + for _, at := range slices.Sorted(maps.Keys(starts)) { + if state[at] == 0 { + walk(at) + } + } + } + slices.Sort(out) + return slices.Compact(out) +} diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/cycle_test.go b/modules/mesh-delivery-assigner/internal/assigngraph/cycle_test.go new file mode 100644 index 00000000..78cc4c82 --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assigngraph/cycle_test.go @@ -0,0 +1,36 @@ +package assigngraph + +import ( + "slices" + "testing" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" +) + +// Design 47 G §272 (AS10): a cycle among the waits within one step is found; an acting part acting on itself's other module, two parts +// acting on each other in different steps, and a chain are none. +func TestACycleAmongTheWaitsWithinOneStepIsFound(t *testing.T) { + act := func(node, module, step string) meshwants.Edge { + return meshwants.Edge{Kind: EdgeActing, Node: node, Module: module, Step: step} + } + entry := func(module string, edges ...meshwants.Edge) meshwants.AssignmentEntry { + return meshwants.AssignmentEntry{Node: "ace", Module: module, Edges: edges} + } + for _, c := range []struct { + name string + entries []meshwants.AssignmentEntry + want []string + }{ + {"two parts on each other in one step", []meshwants.AssignmentEntry{entry("a", act("ace", "b", "compose")), entry("b", act("ace", "a", "compose"))}, + []string{"compose: ace/a -> ace/b -> ace/a"}}, + {"three in a ring, said once", []meshwants.AssignmentEntry{entry("c", act("ace", "a", "build")), entry("a", act("ace", "b", "build")), entry("b", act("ace", "c", "build"))}, + []string{"build: ace/a -> ace/b -> ace/c -> ace/a"}}, + {"the indexer taking in the checker, the checker checking the indexer", []meshwants.AssignmentEntry{entry("checker", act("ace", "indexer", "take-in")), entry("indexer", act("ace", "checker", "check"))}, nil}, + {"a chain", []meshwants.AssignmentEntry{entry("a", act("ace", "b", "compose")), entry("b", act("ace", "c", "compose")), entry("c")}, nil}, + {"provisioning edges are no wait of a step", []meshwants.AssignmentEntry{entry("a", meshwants.Edge{Kind: EdgeProvisioning, Node: "ace", Module: "b"}), entry("b", meshwants.Edge{Kind: EdgeProvisioning, Node: "ace", Module: "a"})}, nil}, + } { + if got := Cycles(c.entries); !slices.Equal(got, c.want) { + t.Errorf("%s: cycles %v, want %v", c.name, got, c.want) + } + } +}