From 1387b10e78930bdf1348356d2b7822362a7250e5 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:21:01 +0100 Subject: [PATCH] Take a missing graph state as no graph yet and a Publisher's directory as the nearest module above it The indexer's recompute and merge failed on listing a graph bucket not yet on the bus, and a package published from inside a module's directory had no publisher, so the dependents of a build module missed what imports its packages (review findings 3 and 4 of L3). Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../internal/index/graph.go | 27 +++++++++++++------ .../internal/index/index_test.go | 4 +++ sdk/graph/graph.go | 21 ++++++++++++--- sdk/graph/graph_test.go | 18 +++++++++++-- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/graph.go b/modules/mesh-delivery-indexer/internal/index/graph.go index f9195d3b..d50caa64 100644 --- a/modules/mesh-delivery-indexer/internal/index/graph.go +++ b/modules/mesh-delivery-indexer/internal/index/graph.go @@ -2,12 +2,14 @@ package index import ( "context" + "errors" "fmt" "maps" "slices" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-delivery/sdk/graph" ) @@ -60,11 +62,18 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) graph. } // writeGraph writes the graph node of every module of edges whose value changed, at most GraphMost of them, then -// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing. +// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing, and neither does a +// graph state that is not on the bus yet (process.ErrNotYet; main.go: until then no graph is written), said in the log; +// a listing that fails otherwise skips the deletion pass, said, and never fails the merge or the recompute that wrote. func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) error { if ix.Graph == nil { return nil } + keys, listErr := ix.Graph.Keys(ctx) + if errors.Is(listErr, process.ErrNotYet) { + ix.logf("the module graph is not written: its state is not on the bus yet") + return nil + } names := slices.Sorted(maps.Keys(edges)) if len(names) > GraphMost { ix.logf("the module graph holds at most %d modules: %d are known, so %s and the %d after it are not written", @@ -104,19 +113,21 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) } } held, err := ix.Modules.Keys(ctx) - if err != nil { - return fmt.Errorf("listing the modules for the graph: %w", err) - } - keys, err := ix.Graph.Keys(ctx) - if err != nil { - return fmt.Errorf("listing the graph: %w", err) + switch { + case listErr != nil: + ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the graph: %v", listErr) + return nil + case err != nil: + ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the modules: %v", err) + return nil } for _, key := range keys { if slices.Contains(held, key) { continue } if err := write(ctx, ix.Graph, key, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }); err != nil { - return fmt.Errorf("graph.%s: %w", key, err) + ix.logf("graph.%s is not deleted: %v", key, err) + continue } ix.logf("graph.%s deleted: the modules state holds no module %s", key, key) } diff --git a/modules/mesh-delivery-indexer/internal/index/index_test.go b/modules/mesh-delivery-indexer/internal/index/index_test.go index e6f9da4d..b584aa67 100644 --- a/modules/mesh-delivery-indexer/internal/index/index_test.go +++ b/modules/mesh-delivery-indexer/internal/index/index_test.go @@ -19,6 +19,7 @@ const ( // key under the indexer; unbound, when set, is every read's error (a state not yet on the bus). type memStore struct { unbound error + keysErr error // the listing's error vals map[string][]byte revs map[string]uint64 next uint64 @@ -36,6 +37,9 @@ func (m *memStore) Get(_ context.Context, key string) ([]byte, uint64, bool, err return v, m.revs[key], ok, nil } func (m *memStore) Keys(context.Context) ([]string, error) { + if m.keysErr != nil { + return nil, m.keysErr + } var keys []string for k := range m.vals { keys = append(keys, k) diff --git a/sdk/graph/graph.go b/sdk/graph/graph.go index 6648ced8..b0c7951c 100644 --- a/sdk/graph/graph.go +++ b/sdk/graph/graph.go @@ -230,8 +230,9 @@ func (g Graph) Requirers(provision string) []string { return out } -// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository), -// or "" when the graph holds none. +// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository): +// the module whose directory is the package's directory or the nearest one above it (the longest match, a package +// published from inside a module's directory belonging to that module), or "" when the graph holds none. func (g Graph) Publisher(pkg string) string { repository, dir, ok := delivery.PackageRepository(pkg) if !ok { @@ -240,12 +241,24 @@ func (g Graph) Publisher(pkg string) string { if dir == "" { dir = delivery.RootPath } + best, bestLen := "", -1 for _, name := range slices.Sorted(maps.Keys(g)) { - if n := g[name]; n.Repository == repository && n.Path == dir { + n := g[name] + if n.Repository != repository { + continue + } + switch { + case n.Path == dir: return name + case n.Path == delivery.RootPath: + if bestLen < 0 { + best, bestLen = name, 0 + } + case strings.HasPrefix(dir, n.Path+"/") && len(n.Path) > bestLen: + best, bestLen = name, len(n.Path) } } - return "" + return best } // buildEdges are the modules n's build edges reach: its bases, the publisher of each of its packages, and the build diff --git a/sdk/graph/graph_test.go b/sdk/graph/graph_test.go index 9c34b9db..2848f19c 100644 --- a/sdk/graph/graph_test.go +++ b/sdk/graph/graph_test.go @@ -32,7 +32,11 @@ func TestGraphEdges(t *testing.T) { n.Bases, n.Packages, n.Toolchains = []string{"base"}, []string{"git.novox.be/novox/lib"}, gotool }), "top": node("novox/mesh-catalog", "modules/top", func(n *Node) { n.Bases = []string{"app"} }), - "db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }), + // A package published from inside a module's directory belongs to that module (ADR 0604 ยง1): users imports build-go's. + "users": node("novox/other", "modules/users", func(n *Node) { + n.Packages = []string{"git.novox.be/novox/mesh-catalog/modules/build-go/buildrun"} + }), + "db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }), "web": node("novox/mesh-catalog", "modules/web", func(n *Node) { n.Requires, n.Awaits = []string{"database", "db"}, []string{"database"} }), @@ -40,7 +44,7 @@ func TestGraphEdges(t *testing.T) { for module, want := range map[string][]string{ "base": {"app", "top"}, "lib": {"app", "top"}, - "build-go": {"app", "top"}, // build-go's own toolchain edge reaches itself, which is left out + "build-go": {"app", "top", "users"}, // build-go's own toolchain edge reaches itself, which is left out "top": nil, "db": nil, // a provision edge is no build edge } { @@ -63,6 +67,16 @@ func TestGraphEdges(t *testing.T) { if got := g.Requirers("db"); !reflect.DeepEqual(got, []string{"web"}) { t.Errorf("requirers of db = %v", got) } + for pkg, want := range map[string]string{ + "git.novox.be/novox/mesh-catalog/modules/build-go/buildrun": "build-go", // inside its directory + "git.novox.be/novox/mesh-catalog/modules/build-go": "build-go", // its directory + "git.novox.be/novox/mesh-catalog/modules/build-gone": "", // beside it, not inside + "git.novox.be/novox/lib/sub": "lib", // a root module publishes what is below it + } { + if got := g.Publisher(pkg); got != want { + t.Errorf("publisher of %s = %q, want %q", pkg, got, want) + } + } if g.Publisher("git.novox.be/novox/lib") != "lib" || g.Publisher("git.novox.be/novox/mesh-sdk/go") != "" { t.Error("a package's publisher is not the module at its repository and directory") }