diff --git a/modules/mesh-delivery-indexer/internal/index/graph.go b/modules/mesh-delivery-indexer/internal/index/graph.go index d50caa64..be3a5d1b 100644 --- a/modules/mesh-delivery-indexer/internal/index/graph.go +++ b/modules/mesh-delivery-indexer/internal/index/graph.go @@ -15,8 +15,8 @@ import ( ) // The module graph (novox/hq ADR 0603 §2): at every recompute the indexer writes, in its state graph -// (graph.Bucket), one key per module it holds the inputs of, its build and provision edges as that recompute -// computed them, and deletes the key of a module the modules state no longer holds. A key is written only when its value +// (graph.Bucket), one key per module it holds the inputs of, its build, provision and verb edges as that recompute +// computed them (ADR 0614 §1), the verb edges on a cycle left out over the whole graph (§3), and deletes the key of a module the modules state no longer holds. A key is written only when its value // changes, so its history of 5 is five changes of the module's edges. The delivery view, the checker and the seat // mesh-catalog's verbs read it; nothing stores an order (ADR 0603 §1). @@ -89,13 +89,20 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) } return out } + // The verb edges on a cycle are left out over the whole graph (ADR 0614 §3), every module it knows, so a module past + // GraphMost still closes the cycle it lies on. + whole := graph.Graph{} + for name, e := range edges { + whole[name] = graphNode(e, providers) + } + whole.LeaveOut() for _, name := range names { key, err := graph.Key(name) if err != nil { ix.logf("graph.%s is not written: %v", name, err) continue } - next := graphNode(edges[name], providers) + next := whole[name] err = write(ctx, ix.Graph, key, func(value []byte, found bool) ([]byte, bool, error) { if found { if cur, err := graph.Decode(value); err == nil && graph.Same(cur, next) { diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 2510e054..435c00c6 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "slices" "strings" "testing" "time" @@ -330,6 +331,24 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // t.Errorf("%s moved with a build of build-agent: fp2 = %q, want %s", name, m.FP2, fp) } } + // ADR 0614 §2, §3: the graph names what each module invokes and serves, and leaves out, over the whole graph, the + // verb edges of two modules calling each other, from either side; a third calling one of them keeps its edge. + invoking := func(name, invokes string) mod { + raw := renamed(t, "avahi", name) + return mod{name, strings.Replace(raw, `"version": "1",`, `"version": "1", "invokes": [`+invokes+`],`, 1)} + } + vx, _ := newFormIndexer(t, invoking("caller", `"callee.avahi_status"`), invoking("callee", `"caller.avahi_browse", "*"`), + invoking("third", `"callee.avahi_status"`)) + for name, want := range map[string][2][]string{ + "caller": {{"callee.avahi_status"}, {"callee.avahi_status"}}, + "callee": {{"caller.avahi_browse"}, {"caller.avahi_browse"}}, + "third": {{"callee.avahi_status"}, nil}, + } { + if g := graphOf(t, vx, name); !reflect.DeepEqual(g.Invokes, want[0]) || !reflect.DeepEqual(g.LeftOut, want[1]) || + !slices.Contains(g.Serves, name+".avahi_status") { + t.Errorf("graph.%s = %+v, want it to invoke %v and leave out %v", name, g, want[0], want[1]) + } + } } func TestABuildHeardBeforeAnyToolchainIsReadDropsNoFingerprint(t *testing.T) { // 1727 (review: a restart flapped every FP2)