diff --git a/contracts/go/seats/meshcatalog/contract.go b/contracts/go/seats/meshcatalog/contract.go index 34b65f16..ec2ccf76 100644 --- a/contracts/go/seats/meshcatalog/contract.go +++ b/contracts/go/seats/meshcatalog/contract.go @@ -1034,9 +1034,11 @@ type VersionEdge struct { // the digest of the part pointed to, kinds 1-4 Digest string `json:"digest,omitempty"` // kinds 5 and 6: the acting part, its step and the build that acted, or the verb or word used and the delivered version that serves it - Fact string `json:"fact,omitempty"` - Kind int64 `json:"kind"` - Part ArtifactRef `json:"part"` + Fact string `json:"fact,omitempty"` + // the entry was derived at the first fill (design 47 G section 377), not recorded at a release or a delivery + Filled bool `json:"filled,omitempty"` + Kind int64 `json:"kind"` + Part ArtifactRef `json:"part"` // the edge makes the dependent stale (kinds 1, 3) or only orders it (kinds 2, 4, 5, 6) (section 378) Stale bool `json:"stale"` // the dependency's version, kinds 1-4; "not yet delivered" until the first delivery for 2, 4 and 6 diff --git a/contracts/seats/mesh-catalog/seat.json b/contracts/seats/mesh-catalog/seat.json index 40fba93d..a233e12a 100644 --- a/contracts/seats/mesh-catalog/seat.json +++ b/contracts/seats/mesh-catalog/seat.json @@ -2555,6 +2555,10 @@ "fact": { "type": "string", "description": "kinds 5 and 6: the acting part, its step and the build that acted, or the verb or word used and the delivered version that serves it" + }, + "filled": { + "type": "boolean", + "description": "the entry was derived at the first fill (design 47 G section 377), not recorded at a release or a delivery" } }, "required": [ diff --git a/contracts/ts/seats/meshcatalog/index.d.ts b/contracts/ts/seats/meshcatalog/index.d.ts index dc83114e..dd3e2e5e 100644 --- a/contracts/ts/seats/meshcatalog/index.d.ts +++ b/contracts/ts/seats/meshcatalog/index.d.ts @@ -490,6 +490,8 @@ export interface VersionEdge { "digest"?: string; /** kinds 5 and 6: the acting part, its step and the build that acted, or the verb or word used and the delivered version that serves it */ "fact"?: string; + /** the entry was derived at the first fill (design 47 G section 377), not recorded at a release or a delivery */ + "filled"?: boolean; "kind": number; "part": ArtifactRef; /** the edge makes the dependent stale (kinds 1, 3) or only orders it (kinds 2, 4, 5, 6) (section 378) */ diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index 8b9e0f39..69e3c1d4 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -441,7 +441,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Predefined: awaitedStore{b: predefined}, ConfigCandidates: awaitedStore{b: configCandidates}, Logf: logger.Printf, Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call), ReadOpen: openVia(calls.Call), Packages: awaitedStore{b: packages, js: js, name: index.PackagesBucket}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call), - OwnBuild: ownBuildVia(calls.Call, cred.Node), OpenTries: openTriesVia(calls.Call), OwnBuildState: awaitedStore{b: ownBuildState}, Heard: heardStore, TryRecovery: awaitedStore{b: tryRecovery}, Graph: graph, + OwnBuild: ownBuildVia(calls.Call, cred.Node), OpenTries: openTriesVia(calls.Call), OwnBuildState: awaitedStore{b: ownBuildState}, Heard: heardStore, Placement: placementVia(calls.CallKept), RunningWhole: runningWholeVia(js), TryRecovery: awaitedStore{b: tryRecovery}, Graph: graph, SeatDefinitions: awaitedStore{b: seatDefs}, ReadFile: readFileVia(calls.Call), SaySeatDefinition: saySeats, Message: messageVia(calls.CallKept), Revisions: revisions, EntryIndex: awaitedStore{b: process.AwaitBucket(ctx, js, conn, index.EntryIndexBucket, "no entry is found by its fingerprint", logger.Printf)}, // Message a kept call: a read, called again through a transport fault (ADR 0668 §1) // Each catalog revision may bring another holder of a build seat: the build modules are found again (buildmodules.go). diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/versions.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/versions.go index 1f624d09..ce127695 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/versions.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/versions.go @@ -10,6 +10,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" @@ -159,3 +161,58 @@ func afterRevision(say func(context.Context, meshcatalog.CatalogRevisionEvent) e return err } } + +// placementVia asks the seat mesh-module-assigner's verb placement for one module (A-62), a read. +func placementVia(call toolCaller) index.PlacementAsker { + return func(_ context.Context, module string) ([]meshmoduleassigner.ModulePlacement, error) { + raw, err := call(meshmoduleassigner.Seat+"."+meshmoduleassigner.VerbPlacement, map[string]any{"module": module}) + if err != nil { + return nil, fmt.Errorf("%s.%s: %w", meshmoduleassigner.Seat, meshmoduleassigner.VerbPlacement, err) + } + var a meshmoduleassigner.PlacementAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return nil, fmt.Errorf("the answer of placement is not a module's placements: %w", err) + } + return a.Placements, nil + } +} + +// runningWholeVia reads every subject's last message of the node-engine seat's stream running through an ordered consumer made for +// that one read (design 47 G §556), by node and module. +func runningWholeVia(js jetstream.JetStream) index.RunningReader { + return func(ctx context.Context) (map[string]map[string]nodeengine.Running, error) { + stream, err := js.Stream(ctx, nodeengine.StreamRunning) + if err != nil { + return nil, fmt.Errorf("the stream %s cannot be read whole: %w", nodeengine.StreamRunning, err) + } + cons, err := stream.OrderedConsumer(ctx, jetstream.OrderedConsumerConfig{DeliverPolicy: jetstream.DeliverLastPerSubjectPolicy}) + if err != nil { + return nil, fmt.Errorf("the ordered consumer for the whole read of %s cannot be made: %w", nodeengine.StreamRunning, err) + } + out := map[string]map[string]nodeengine.Running{} + for { + batch, err := cons.FetchNoWait(256) + if err != nil { + return nil, fmt.Errorf("the whole read of %s failed: %w", nodeengine.StreamRunning, err) + } + n := 0 + for m := range batch.Messages() { + n++ + var r nodeengine.Running + if json.Unmarshal(m.Data(), &r) != nil || r.Node == "" || r.Module == "" { + continue + } + if out[r.Node] == nil { + out[r.Node] = map[string]nodeengine.Running{} + } + out[r.Node][r.Module] = r + } + if err := batch.Error(); err != nil { + return nil, fmt.Errorf("the whole read of %s failed: %w", nodeengine.StreamRunning, err) + } + if n == 0 { + return out, nil + } + } + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 608f8c1e..ce1ae9e0 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -193,7 +193,11 @@ type Indexer struct { // Heard keeps what the indexer heard and compares later (A-80): the time a call is unanswered since (steps-since, call-since), // toolchain., delivered., pending.., acted.., each key part escaped as a name // (§73), and the mark versions-filled; nil keeps none. It is the state heard, whose key forge keeps the forge messages handled. - Heard Store + Heard Store + // Placement asks the assigner's verb placement (A-62) and RunningWhole reads the node-engine's stream running whole (§556), both for the + // edge entries of the first fill (§377); nil reads none, and the entries of kinds 2, 4 and 6 then read "not yet delivered". + Placement PlacementAsker + RunningWhole RunningReader judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go) // Message asks the forge bridge for the merged message of a merge, repository, merge commit and pull request number in (the number lets the bridge read a merge made on the forge, not through the gate), the message whole out (a // pending value keeps a reference, not a copy: issue 1585); an error is the bridge's words. Nil asks none. diff --git a/modules/mesh-delivery-indexer/internal/index/pending.go b/modules/mesh-delivery-indexer/internal/index/pending.go index f2c65992..36826d8d 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending.go +++ b/modules/mesh-delivery-indexer/internal/index/pending.go @@ -908,7 +908,7 @@ func (ix *Indexer) dry() *dryRun { // requirements, findings, seat definitions, holds and their bounds are not overlaid, nothing is said, and no hold is // weighed (Holds and Counted nil hold nothing); ReadFile only reads. Requirements: nil, Findings: nil, SeatDefinitions: nil, ReadFile: ix.ReadFile, SaySeatDefinition: nil, - OwnBuildState: nil, Heard: nil, underSeats: true, + OwnBuildState: nil, Heard: nil, Placement: nil, RunningWhole: nil, underSeats: true, // A dry run keeps no catalog graph revision (catalog_revisions.go): its revision is the take-in's to keep. Revisions: nil, SayCatalogRevision: nil, SayFill: nil, EntryIndex: nil, Versions: nil, SayVersionReleased: nil, SayVersionsFilled: nil, Inputs: nil, // A dry run writes no registration and asks no one. diff --git a/modules/mesh-delivery-indexer/internal/index/pending_test.go b/modules/mesh-delivery-indexer/internal/index/pending_test.go index 43a0003c..dccf55ff 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending_test.go +++ b/modules/mesh-delivery-indexer/internal/index/pending_test.go @@ -1131,7 +1131,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) for _, f := range []string{"Store", "Modules", "Manifests", "Predefined", "ConfigCandidates", "Taken", "Verdicts", "OldSections", "Unjudged", "JudgeID", "SayRefused", "SayAccepted", "Packages", "Graph", "Files", "GoSum", "GoMod", "Now", "Builds", "Named", "Trunk", "Read", "Contains", "Logf", "ReadOpen", "OwnBuild", "Message", "Pending", "SayTakeIn", - "Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinition", "OpenTries", "TryRecovery", "OwnBuildState", "Heard", "Revisions", "SayCatalogRevision", "SayFill", "EntryIndex", "Registry", "SeedModules", "Versions", "SayVersionReleased", "SayVersionsFilled", "Inputs"} { + "Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinition", "OpenTries", "TryRecovery", "OwnBuildState", "Heard", "Placement", "RunningWhole", "Revisions", "SayCatalogRevision", "SayFill", "EntryIndex", "Registry", "SeedModules", "Versions", "SayVersionReleased", "SayVersionsFilled", "Inputs"} { named[f] = true } ty := reflect.TypeOf(Indexer{}) diff --git a/modules/mesh-delivery-indexer/internal/index/versions_fill.go b/modules/mesh-delivery-indexer/internal/index/versions_fill.go index 4e4e8367..49d29256 100644 --- a/modules/mesh-delivery-indexer/internal/index/versions_fill.go +++ b/modules/mesh-delivery-indexer/internal/index/versions_fill.go @@ -5,9 +5,12 @@ import ( "encoding/json" "errors" "fmt" + "maps" + "slices" "strconv" "time" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-sdk/go/keys" @@ -127,6 +130,12 @@ func (ix *Indexer) FillVersions(ctx context.Context, ask VersionsAsker) error { } now := ix.now().UTC() filled := 0 + built := map[string]meshcatalog.VersionEntry{} + claimed := map[string]time.Time{} + trunk := map[string]meshcatalog.CatalogEntry{} + for _, e := range entries { + trunk[e.Module] = e + } for module, fs := range best { v := meshcatalog.VersionEntry{V: int64(keys.Version), Module: module, Version: fs[0].v.Version, At: &now} for _, f := range fs { @@ -134,7 +143,29 @@ func (ix *Indexer) FillVersions(ctx context.Context, ask VersionsAsker) error { if n, err := strconv.ParseInt(f.v.Revision, 10, 64); err == nil && n > v.Source { v.Source = n } + if f.v.At.After(claimed[module]) { + claimed[module] = f.v.At + } } + built[module] = v + } + // The edge entries (§377), each marked filled: where the stream running or the placement cannot be read the entries of kinds 2, 4 + // and 6 read "not yet delivered", as at a release, and the first delivery writes them. + var running map[string]map[string]nodeengine.Running + if ix.RunningWhole != nil { + if running, err = ix.RunningWhole(ctx); err != nil { + ix.logf("versions fill: the stream running is not read whole: %v", err) + running = nil + } + } + answers := map[string]BuilderVersionsAnswer{} + for _, module := range slices.Sorted(maps.Keys(built)) { + v := built[module] + v.Edges = ix.filledEdges(ctx, trunk[module], claimed[module], ask, trunk, built, running, answers) + built[module] = v + } + for _, module := range slices.Sorted(maps.Keys(built)) { + v := built[module] data, err := json.Marshal(v) if err != nil { return err @@ -142,6 +173,7 @@ func (ix *Indexer) FillVersions(ctx context.Context, ask VersionsAsker) error { if err := ix.Versions.Create(ctx, meshcatalog.VersionsByModuleVersionKey(module, v.Version), data); err != nil && !errors.Is(err, ErrConflict) { return fmt.Errorf("the version %s %s is not kept: %w", module, v.Version, err) } + ix.keepFilledFacts(ctx, v, trunk[module], built, now) filled++ } if ix.SayVersionsFilled != nil { diff --git a/modules/mesh-delivery-indexer/internal/index/versions_fill_edges.go b/modules/mesh-delivery-indexer/internal/index/versions_fill_edges.go new file mode 100644 index 00000000..a47075f6 --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/versions_fill_edges.go @@ -0,0 +1,159 @@ +package index + +import ( + "cmp" + "context" + "encoding/json" + "slices" + "strings" + "time" + + "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The edge entries of the first fill (design 47 G §377, A-78 c): each module's newest version gets its entries once, each marked +// filled. Kinds 1 and 3 come from the dependency version published latest before the version's build was claimed, read with +// the times of builder_versions, and its part's digest; kinds 2, 4 and 6 from the version every machine placing the dependency +// runs, present or away, the machines from the assigner's placement (A-62) and each node's last running from the node-engine seat's +// stream read whole (§556); kind 5 as done for every acting part's running build. + +// PlacementAsker asks the seat mesh-module-assigner's verb placement for a module (a read, A-62). +type PlacementAsker func(ctx context.Context, module string) ([]meshmoduleassigner.ModulePlacement, error) + +// RunningReader reads the node-engine seat's stream running whole, every subject's last message (§556): by node, then module. +type RunningReader func(ctx context.Context) (map[string]map[string]nodeengine.Running, error) + +// filledEdges are the edge entries of the entry's newest version, which was filled as built; claimed is when its build was claimed. +func (ix *Indexer) filledEdges(ctx context.Context, entry meshcatalog.CatalogEntry, claimed time.Time, ask VersionsAsker, trunk map[string]meshcatalog.CatalogEntry, + built map[string]meshcatalog.VersionEntry, running map[string]map[string]nodeengine.Running, answers map[string]BuilderVersionsAnswer) []meshcatalog.VersionEdge { + var out []meshcatalog.VersionEdge + seen := func(e meshcatalog.VersionEdge) bool { + return slices.ContainsFunc(out, func(x meshcatalog.VersionEdge) bool { return x.Kind == e.Kind && x.Part == e.Part && x.Word == e.Word }) + } + for _, g := range entry.Edges { + part := cmp.Or(g.Part, g.From) + module := strings.SplitN(part, ":", 2)[0] + if part == "" || module == entry.Module { + continue + } + e := meshcatalog.VersionEdge{Kind: g.Kind, Part: part, Word: g.Word, Filled: true} + switch g.Kind { + case 1, 3: + dep, ok := trunk[module] + if !ok { + continue + } + var name string + for _, a := range dep.Artifacts { + if a.Ref == part { + name = versionNameOf(module, a) + } + } + if name == "" { + continue + } + ans, ok := answers[name] + if !ok { + a, err := ask(ctx, name) + if err != nil { + continue + } + ans, answers[name] = a, a + } + var best *BuilderVersion + for i, v := range ans.Versions { + if _, ok := versionParts(v.Version); !ok || v.State != "published" || v.At.After(claimed) { + continue + } + if best == nil || v.At.After(best.At) { + best = &ans.Versions[i] + } + } + if best == nil { + continue + } + e.Version, e.Digest, e.Stale = best.Version, best.Digest, true + case 2, 4, 6: + e.Version = notYetDelivered + if v, ok := ix.runningVersion(ctx, module, trunk, built, running); ok { + e.Version = v.Version + for _, a := range v.Artifacts { + if a.Artifact == part { + e.Digest = a.Digest + } + } + } + case 5: + e.Fact = "acted for the running build " + entry.Fp + default: + continue + } + if !seen(e) { + out = append(out, e) + } + } + return out +} + +// runningVersion is the version of a module that every machine placing it runs, present or away, when each runs the newest +// build; false where no machine places it, a placed machine runs none or another build, or the placement is not answered. +func (ix *Indexer) runningVersion(ctx context.Context, module string, trunk map[string]meshcatalog.CatalogEntry, built map[string]meshcatalog.VersionEntry, + running map[string]map[string]nodeengine.Running) (meshcatalog.VersionEntry, bool) { + v, ok := built[module] + e, known := trunk[module] + if !ok || !known || e.Fp == "" || ix.Placement == nil { + return v, false + } + placements, err := ix.Placement(ctx, module) + if err != nil { + ix.logf("versions fill: the placement of %s is not answered: %v", module, err) + return v, false + } + nodes := 0 + for _, p := range placements { + for _, n := range p.Nodes { + nodes++ + r, ok := running[n][module] + if !ok || r.Stopped || r.Fingerprint != e.Fp { + return v, false + } + } + } + return v, nodes > 0 +} + +// keepFilledFacts keeps what the first fill derived of a version as heard: for each entry of kinds 2, 4 and 6 read as delivered, +// the dependency's version as its newest delivered one (A-68), and for each kind-5 edge the act for the running build (A-78 a). +func (ix *Indexer) keepFilledFacts(ctx context.Context, v meshcatalog.VersionEntry, entry meshcatalog.CatalogEntry, built map[string]meshcatalog.VersionEntry, at time.Time) { + for _, e := range v.Edges { + switch e.Kind { + case 2, 4, 6: + dep := built[strings.SplitN(e.Part, ":", 2)[0]] + if e.Version == notYetDelivered || dep.Version != e.Version { + continue + } + parts := map[string]string{} + for _, a := range dep.Artifacts { + parts[a.Artifact] = a.Digest + } + d := meshwants.VersionDeliveredEvent{V: 1, Module: dep.Module, Version: dep.Version, Parts: parts, At: &at} + if err := ix.keepNewest(ctx, DeliveredPrefix+keys.EscapeName(dep.Module), d, func([]byte) bool { return false }); err != nil { + ix.logf("versions fill: the delivered version of %s is not kept: %v", dep.Module, err) + } + case 5: + data, _ := json.Marshal(Acted{V: 1, Part: e.Part, Build: entry.Fp, How: "filled", At: at}) + if err := write(ctx, ix.Heard, ActedKey(e.Part, v.Module), func(_ []byte, found bool) ([]byte, bool, error) { + if found { + return nil, false, nil + } + return data, false, nil + }); err != nil { + ix.logf("versions fill: the act for %s is not kept: %v", v.Module, err) + } + } + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/versions_test.go b/modules/mesh-delivery-indexer/internal/index/versions_test.go index f35788f0..224982dc 100644 --- a/modules/mesh-delivery-indexer/internal/index/versions_test.go +++ b/modules/mesh-delivery-indexer/internal/index/versions_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" @@ -411,3 +413,73 @@ func TestOutdatedAnswersKinds3And5And6(t *testing.T) { t.Errorf("kind 6 after a version that serves no more: %+v", o) } } + +// Design 47 G §377, A-78 c: the first fill gives each module's newest version its edge entries, each marked filled: kind 1 from the +// dependency version published latest before the build was claimed, kinds 2, 4 and 6 from the version every placing machine runs, +// read from the assigner's placement and the stream running, kind 5 as acted for the running build; so the first outdated says only what is truly behind. +func TestTheFirstFillGivesTheNewestVersionsTheirFilledEdges(t *testing.T) { + ctx := context.Background() + t0 := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC) + placed := map[string][]string{"prov": {"n1", "n2"}} + ix := &Indexer{Versions: newMem(), Heard: newMem(), Revisions: newMemObjects(1 << 20), Now: func() time.Time { return t0 }, + Placement: func(_ context.Context, m string) ([]meshmoduleassigner.ModulePlacement, error) { + return []meshmoduleassigner.ModulePlacement{{Branch: "trunk", Nodes: placed[m]}}, nil + }, + RunningWhole: func(context.Context) (map[string]map[string]nodeengine.Running, error) { + return map[string]map[string]nodeengine.Running{"n1": {"prov": {Fingerprint: "fp-prov"}}, "n2": {"prov": {Fingerprint: "fp-prov"}}}, nil + }} + entries := map[string]meshcatalog.CatalogEntry{ + "lib": {Module: "lib", Manifest: map[string]any{}, Fp: "fp-lib", Artifacts: []meshcatalog.Artifact{{Ref: "lib:pkg", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: "x/lib"}}}}, + "actor": {Module: "actor", Manifest: map[string]any{}, Fp: "fp-actor", Artifacts: []meshcatalog.Artifact{{Ref: "actor:program"}}}, + "prov": {Module: "prov", Manifest: map[string]any{}, Fp: "fp-prov", Artifacts: []meshcatalog.Artifact{{Ref: "prov:program"}}}, + "app": {Module: "app", Manifest: map[string]any{}, Fp: "fp-app", Artifacts: []meshcatalog.Artifact{{Ref: "app:program"}}, + Edges: []meshcatalog.Edge{{Kind: 1, From: "lib:pkg", To: "app:program", Part: "lib:pkg", Stale: true}, + {Kind: 4, From: "prov:program", To: "app:program", Part: "prov:program"}, + {Kind: 5, From: "actor:program", To: "app:program", Part: "actor:program"}}}, + } + rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, entries) + if err != nil { + t.Fatal(err) + } + if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil { + t.Fatal(err) + } + ask := func(_ context.Context, name string) (BuilderVersionsAnswer, error) { + pub := func(v, d string, at time.Time) BuilderVersion { + return BuilderVersion{Version: v, State: "published", Digest: d, Revision: "9", At: at} + } + switch name { + case "x/lib": + return BuilderVersionsAnswer{Versions: []BuilderVersion{pub("0.1.1", "sha256:l1", t0), pub("0.1.2", "sha256:l2", t0.Add(2*time.Hour))}}, nil + case "app/program": + return BuilderVersionsAnswer{Versions: []BuilderVersion{pub("0.3.1", "sha256:a1", t0.Add(time.Hour))}}, nil + } + return BuilderVersionsAnswer{Versions: []BuilderVersion{pub("0.9.1", "sha256:"+name, t0)}}, nil + } + if err := ix.FillVersions(ctx, ask); err != nil { + t.Fatal(err) + } + app, _ := ModuleVersions(ctx, ix.Versions, "app") + if len(app) != 1 || len(app[0].Edges) != 3 { + t.Fatalf("app's filled version: %+v", app) + } + for _, e := range app[0].Edges { + if !e.Filled { + t.Errorf("a filled entry is not marked: %+v", e) + } + switch e.Kind { + case 1: + if e.Version != "0.1.1" || e.Digest != "sha256:l1" || !e.Stale { + t.Errorf("kind 1 is the version published before the build was claimed: %+v", e) + } + case 4: + if e.Version != "0.9.1" || e.Digest != "sha256:prov/program" { + t.Errorf("kind 4 is the version every machine runs: %+v", e) + } + } + } + // lib's 0.1.2 was published after app's build was claimed: that is behind, and nothing else (the delivered kind 4 and the act of kind 5 are not) + if a, err := ix.Outdated(ctx, meshcatalog.OutdatedInput{}); err != nil || len(a.Behind) != 1 || a.Behind[0].Kind != 1 { + t.Errorf("outdated after the first fill: %+v, %v", a.Behind, err) + } +} diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index e18f3fa8..c7c45db8 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -161,7 +161,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // The registry seats' published fill each (§413: no consume of version.numbered) package's series (DLV-I5, design 51 *The versions*). if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-builds.package-published", "mesh-builds.toolchain-agreed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-checker.preview.written", "mesh-delivery-checker.try.ended", "mesh-delivery-checker.try.started", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**", - "mesh-delivery.source-moved", "mesh-wants.version-delivered", "mesh-wants.wait-ended", "npm-package-registry.published", "operator-channel.decided"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + "mesh-delivery.source-moved", "mesh-wants.version-delivered", "mesh-wants.wait-ended", "node-engine.running", "npm-package-registry.published", "operator-channel.decided"}; !reflect.DeepEqual(sorted(m.Consumes), want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" { @@ -173,7 +173,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths: // ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a // commit is in another (issue 1900). - if want := []string{"mesh-delivery-assigner.assignments", "seat:git.trees", "seat:git.file", "seat:git.contains", "seat:git.open-pulls", "seat:git.branch", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.steps", "seat:node-build-node.steps", "seat:node-build-image.steps", "mesh-delivery-builder.builder_versions"}; !reflect.DeepEqual(m.Invokes, want) { + if want := []string{"mesh-delivery-assigner.assignments", "seat:git.trees", "seat:git.file", "seat:git.contains", "seat:git.open-pulls", "seat:git.branch", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-module-assigner.placement", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.steps", "seat:node-build-node.steps", "seat:node-build-image.steps", "mesh-delivery-builder.builder_versions"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle @@ -195,7 +195,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TryCandidatesTool, index.DropTryCandidateTool, index.RecoverTriesTool, index.TakeInMergeTool, index.FingerprintsAgainTool, // issue 1966; 1585/T12, T13 (ADR 0633 §5) // milestone 47/G W4.12 (design 47, the indexer's tools): the catalog graph revisions read and steered. index.CatalogRevisionTool, index.CatalogRevisionsTool, index.CandidateGraphsTool, index.RecomputeTool, index.DropCandidateGraphTool, - index.EntryIndexTool, // design 47 G §675 + index.EntryIndexTool, // design 47 G §675 index.ProposalsTool, index.ProposalDropTool, index.SeedAgainTool, index.InputsTool) // design 47 G §667, §8 if !reflect.DeepEqual(m.Tools, want) || len(want) != 46 || slices.Contains(m.Tools, "modules") { t.Errorf("tools = %v, want %v", m.Tools, want) diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 7daa9d0e..dde0c85e 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -86,6 +86,7 @@ "mesh-builds.package-published", "mesh-builds.toolchain-agreed", "mesh-wants.version-delivered", + "node-engine.running", "mesh-wants.wait-ended", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**", @@ -157,6 +158,7 @@ "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", + "seat:mesh-module-assigner.placement", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence",