diff --git a/cmd/mesh-controller/builder_on_the_bus_test.go b/cmd/mesh-controller/builder_on_the_bus_test.go index fe277dbd..b17ff06e 100644 --- a/cmd/mesh-controller/builder_on_the_bus_test.go +++ b/cmd/mesh-controller/builder_on_the_bus_test.go @@ -51,6 +51,10 @@ func aBuilderOnTheBus(t *testing.T) builderOnTheBus { servingBus.Store(js) t.Cleanup(func() { servingBus.Store(nil) }) withTrunkList(t, true) + // The indexer on this bus is read, not the "not held" a mesh with no bus reads (aMeshOnTheStore). + wasIndexed := indexedModule + indexedModule = theIndexedModule + t.Cleanup(func() { indexedModule = wasIndexed }) return b } @@ -130,3 +134,6 @@ func asksWithPaths(t *testing.T) *[][3]string { t.Cleanup(func() { askABuild = was }) return &asked } + +// theIndexedModule is the look at the indexer as the controller makes it, kept before any test replaces it. +var theIndexedModule = indexedModule diff --git a/cmd/mesh-controller/chain.go b/cmd/mesh-controller/chain.go index 0f648a9d..fa28d8ae 100644 --- a/cmd/mesh-controller/chain.go +++ b/cmd/mesh-controller/chain.go @@ -51,55 +51,13 @@ func attachTheChain(ctx context.Context, inv *inventory.Inventory, server *link. fmt.Printf("chain: the builds are not laid out under their modules yet, and none is read until they are: %v\n", err) go layBuildsOutSoon(ctx, inv) } - // And the requests the move of delivery 1 found never handed over, asked again once (askAgain), once it is serving. - go askAgainSoon(ctx, inv) + // The requests the move of delivery 1 found never handed over are not asked again: the controller asks no build + // (novox/hq ADR 0501; issue 1851, A10), and the builder builds every fingerprint the indexer writes. // And what delivery 1 recorded of what each machine was sent, copied onto the keys of the assignments (CopySentBuilds). go copySentBuildsSoon(ctx, inv) return nil } -// askAgain asks once more for each build request the move found never handed over (novox/hq issue 981 note 7 Q5): the -// requests a merge could not ask for, which a person's hand `build` would otherwise have to repeat. **At most once**: a -// request is claimed before it is asked and settled after, so a crash between loses the ask, which is where it stood. -func askAgain(ctx context.Context, inv *inventory.Inventory) { - pending, err := inv.RequestsToAskAgain(ctx) - if err != nil { - fmt.Printf("chain: the requests never handed over cannot be read: %v\n", err) - return - } - for _, r := range pending { - if claimed, err := inv.ClaimToAskAgain(ctx, r.ID); err != nil || !claimed { - if err != nil { - fmt.Printf("chain: %s cannot be claimed to be asked again: %v\n", r.ID, err) - } - continue - } - id, err := askABuild(ctx, buildSource{Repository: r.Repository, Seat: r.Seat}, r.Path, r.Ref) - if err != nil { - if serr := inv.AskedAgainAs(ctx, r.ID, "not asked again: "+err.Error()); serr != nil { - fmt.Printf("chain: %s was not asked again, and that could not be kept: %v\n", r.ID, serr) - } - fmt.Printf("chain: %s was never handed over and cannot be asked again: %v\n", r.ID, err) - continue - } - recordBuildRequest(ctx, inv, inventory.BuildRequest{ID: id, Repository: r.Repository, Seat: r.Seat, Path: r.Path, - Ref: r.Ref, Commit: r.Commit, For: r.For}) - if err := inv.AskedAgainAs(ctx, r.ID, id); err != nil { - fmt.Printf("chain: %s was asked again as %s, and that could not be kept: %v\n", r.ID, id, err) - } - fmt.Printf("chain: %s, never handed over, is asked again as %s\n", r.ID, id) - } -} - -// askAgainSoon waits for the controller to be serving, which an ask needs, and then asks again. -func askAgainSoon(ctx context.Context, inv *inventory.Inventory) { - select { - case <-ctx.Done(): - case <-time.After(time.Minute): - askAgain(ctx, inv) - } -} - // kindSentBuildsNotCopied is the condition of the one-time copy of node.sent_builds onto the assignments' keys that has not // succeeded: until it has, a module an assignment pulled in may be missing from what a machine is read to run. const kindSentBuildsNotCopied = "sent-builds-not-copied" diff --git a/cmd/mesh-controller/mesh_for_test.go b/cmd/mesh-controller/mesh_for_test.go index 68be9da6..43f73c75 100644 --- a/cmd/mesh-controller/mesh_for_test.go +++ b/cmd/mesh-controller/mesh_for_test.go @@ -13,6 +13,7 @@ import ( "github.com/novox/mesh-controller/internal/conditions" "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/licences" @@ -58,6 +59,13 @@ func aMeshOnTheStore(t *testing.T) *stores { // A condition store of its own, held in memory (novox/hq to-be 45 §2): status leads with what is // open, and a mesh with no bus would otherwise read as one whose conditions cannot be read. withConditionsInMemory(t) + // The indexer holds no module unless a test puts one on its bus (aBuilderOnTheBus): an assignment's look at it + // reads "not held" on a mesh with no bus of its own. + wasIndexed := indexedModule + indexedModule = func(context.Context, string) (sdkdelivery.Module, bool, error) { + return sdkdelivery.Module{}, false, nil + } + t.Cleanup(func() { indexedModule = wasIndexed }) for _, m := range provided { if err := open.inventory.Provide(t.Context(), m); err != nil { t.Fatal(err) diff --git a/cmd/mesh-controller/new_modules.go b/cmd/mesh-controller/new_modules.go index 75871383..58d88cd3 100644 --- a/cmd/mesh-controller/new_modules.go +++ b/cmd/mesh-controller/new_modules.go @@ -57,15 +57,21 @@ func takeNewModules(ctx context.Context, open *stores, only ...string) { return } defer closeKeys() - takeNewModulesWith(ctx, open.inventory, keys, only...) + for _, module := range takeNewModulesWith(ctx, open.inventory, keys, only...) { + // An assignment waiting for it is made now (ADR 0501 §6; issue 1851, A10). + for _, line := range settlePendingOnKey(ctx, open, module) { + fmt.Println(line) + } + } } -// takeNewModulesWith is takeNewModules on the buckets handed in. -func takeNewModulesWith(ctx context.Context, inv *inventory.Inventory, keys newModuleReads, only ...string) { +// takeNewModulesWith is takeNewModules on the buckets handed in; it answers the modules it registered. +func takeNewModulesWith(ctx context.Context, inv *inventory.Inventory, keys newModuleReads, only ...string) []string { entries, err := inv.Catalogued(ctx) if err != nil { fmt.Printf("new modules: the catalogue cannot be read: %v\n", err) - return + // empty-on-error: said above; nothing is registered this time, and the next write of a key looks again + return nil } held := map[string]bool{} for _, e := range entries { @@ -74,7 +80,8 @@ func takeNewModulesWith(ctx context.Context, inv *inventory.Inventory, keys newM values, unread, err := keys.Modules(ctx) if err != nil { fmt.Printf("new modules: the indexer's modules cannot be read: %v\n", err) - return + // empty-on-error: said above; nothing is registered this time, and the next write of a key looks again + return nil } for module, err := range unread { if !held[module] && (len(only) == 0 || slices.Contains(only, module)) { @@ -88,12 +95,14 @@ func takeNewModulesWith(ctx context.Context, inv *inventory.Inventory, keys newM } } sort.Strings(names) + var registered []string for _, module := range names { v := values[module] _, _, err := takeInKey(ctx, inv, keys, keyTake{Module: module, Trunk: v, Ask: registrationAsk{Merge: v.Commit}}) switch { case err == nil: forgetNewModuleWait(module) + registered = append(registered, module) fmt.Printf("%s is new in %s at %.8s: registered from the builder's done key, assigned nowhere\n", module, v.Repository, v.Commit) case errors.Is(err, errKeyWaits): @@ -107,6 +116,7 @@ func takeNewModulesWith(ctx context.Context, inv *inventory.Inventory, keys newM Why: "not registered: " + err.Error()}) } } + return registered } // newModuleReads is what the take-in of new modules reads of the builder and the indexer. diff --git a/cmd/mesh-controller/pending.go b/cmd/mesh-controller/pending.go index a5ec7359..724a6b1a 100644 --- a/cmd/mesh-controller/pending.go +++ b/cmd/mesh-controller/pending.go @@ -9,6 +9,9 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + "github.com/novox/mesh-controller/internal/builderkeys" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" @@ -85,8 +88,30 @@ const ( unknownModule = iota buildInFlight knownNotBuilt + // keyComing is a module the indexer holds and the catalogue does not yet: registered from the builder's done key + // of it (ADR 0501 §6), which nobody asks. + keyComing ) +// keyPendingPrefix opens the build a pending assignment waits for when it waits for a module's key: no build is asked +// (ADR 0501 §5, §6; issue 1851, A10), so it names the module's key, not a build id. +const keyPendingPrefix = "key:" + +// indexedModule is the indexer's trunk value of a module the catalogue does not hold, found when it holds one. A +// variable so a test needs no bus. +var indexedModule = func(ctx context.Context, module string) (sdkdelivery.Module, bool, error) { + keys, closeKeys, err := openBuilderKeys() + if err != nil { + return sdkdelivery.Module{}, false, err + } + defer closeKeys() + v, err := keys.ModuleAt(ctx, module, "") + if errors.Is(err, builderkeys.ErrNoValue) { + return sdkdelivery.Module{}, false, nil + } + return v, err == nil, err +} + // notHeld is where a module the catalogue does not hold stands: its case, the build request it was read // from, and the sentence that says it. type notHeld struct { @@ -96,7 +121,24 @@ type notHeld struct { } // whereIs looks for a module the catalogue does not hold among the build requests the controller keeps. -func whereIs(ctx context.Context, inv *inventory.Inventory, module string, now time.Time) (notHeld, error) { +// notHeldReads is what whereIs reads of the records (*inventory.Inventory, or a test's). +type notHeldReads interface { + RequestsNamed(ctx context.Context, name string) ([]inventory.RequestOutcome, error) + Catalogue(ctx context.Context) (map[string]catalogue.Manifest, error) + RequestedNames(ctx context.Context) ([]string, error) +} + +func whereIs(ctx context.Context, inv notHeldReads, module string, now time.Time) (notHeld, error) { + // **Its key is coming** (ADR 0501 §6): the indexer holds it, and the controller registers it from the builder's done + // key of it. Read before the build requests of before R4e, which nothing writes any more. + if v, held, err := indexedModule(ctx, module); err != nil { + return notHeld{}, fmt.Errorf("whether the indexer holds %s cannot be read: %w", module, err) + } else if held { + return notHeld{kind: keyComing, request: inventory.RequestOutcome{BuildRequest: inventory.BuildRequest{ + ID: keyPendingPrefix + module, Repository: v.Repository, Seat: gitSeat, Path: keyPath(v)}}, + said: fmt.Sprintf("%s is not registered yet: the indexer holds it in %s at %.8s, and it is registered from "+ + "the builder's done key of it, which no one asks", module, v.Repository, v.Commit)}, nil + } requests, err := inv.RequestsNamed(ctx, module) if err != nil { return notHeld{}, err @@ -169,6 +211,12 @@ func notRegistered(ctx context.Context, open *stores, node, module string, opts return "", err } switch where.kind { + case keyComing: + lead := where.said + if opts.Build { + lead += "\n build \"true\" asks nothing: the builder builds every fingerprint the indexer writes (ADR 0501 §6)" + } + return keepPending(ctx, open, node, module, where.request, waiting, lead) case buildInFlight: lead := where.said if opts.Build { @@ -183,30 +231,20 @@ func notRegistered(ctx context.Context, open *stores, node, module string, opts also = fmt.Sprintf("\n %s already has a pending assignment of %s, waiting for build %s; build "+ "\"true\" asks for a new build and makes it wait for that one", node, module, waiting.Build) } - return "", fmt.Errorf("%w: %s%s\n assign it again with build \"true\" to ask for its build and keep this "+ - "assignment until the build registers it; or `build` it (repository %s, path %s) and assign it "+ - "once it is registered", inventory.ErrNoSuchModule, where.said, also, last.Repository, orRoot(last.Path)) + return "", fmt.Errorf("%w: %s%s\n it is registered from the builder's done key once a merge adds it to %s "+ + "(path %s) and the indexer holds it, or at the controller's terminal; assign it then", + inventory.ErrNoSuchModule, where.said, also, last.Repository, orRoot(last.Path)) } - if waiting != nil && waiting.State == inventory.PendingApplying { - return "", fmt.Errorf("%s is being assigned %s now; nothing was asked", node, module) - } - source := buildSource{Repository: last.Repository, Seat: last.Seat} - id, err := askABuild(ctx, source, last.Path, last.Ref) - if err != nil { - return "", fmt.Errorf("%s\n its build could not be asked for: %w; nothing was assigned", where.said, err) - } - asked := inventory.RequestOutcome{BuildRequest: inventory.BuildRequest{ID: id, Repository: last.Repository, - Seat: last.Seat, Path: last.Path, Ref: last.Ref, For: "assign", At: now.UTC()}} - recordBuildRequest(ctx, inv, asked.BuildRequest) - lead := fmt.Sprintf("%s\n build %s of %s is asked for now; %s can be assigned once it registers it", - where.said, id, requestSource(asked), module) - return keepPending(ctx, open, node, module, asked, waiting, lead) + // **No build is asked** (ADR 0501 §5, §6): a module the indexer does not hold has no key coming. + return "", fmt.Errorf("%w: %s\n build \"true\" asks no build: the controller asks none (ADR 0501 §5); a module "+ + "is registered from the builder's done key once a merge adds it to a repository the catalogue builds from, or "+ + "at the controller's terminal, and the indexer holds no %s", inventory.ErrNoSuchModule, where.said, module) } answer := strings.TrimPrefix(where.said, inventory.ErrNoSuchModule.Error()) + - "\n a module in a repository is built with `build` (its repository and path), then assigned" + "\n a module in a repository is registered from the builder's done key once a merge adds it, then assigned" if opts.Build { - answer += "\n build \"true\" asks for nothing here: the controller has no build request saying where a " + - "module of that name is" + answer += "\n build \"true\" asks for nothing: the controller asks no build (ADR 0501 §5), and the indexer " + + "holds no module of that name" } return "", fmt.Errorf("%w%s", inventory.ErrNoSuchModule, answer) } @@ -316,7 +354,7 @@ func clock(t time.Time) string { return t.Local().Format("2006-01-02 15:04:05 MS // closestNames is up to three names near the one asked: registered modules and directories the controller // asked to build. -func closestNames(ctx context.Context, inv *inventory.Inventory, name string) []string { +func closestNames(ctx context.Context, inv notHeldReads, name string) []string { var candidates []string if shelf, err := inv.Catalogue(ctx); err == nil { for m := range shelf { @@ -535,6 +573,19 @@ func settlePending(ctx context.Context, open *stores, now time.Time) []string { } continue } + if strings.HasPrefix(p.Build, keyPendingPrefix) { + why, err := keyPendingEnded(ctx, p.Module) + if err != nil { + say("the key %s waits for cannot be read: %v", p.Module, err) + continue + } + if why != "" { + if line := expirePending(ctx, inv, p, why); line != "" { + said = append(said, line) + } + } + continue + } requests, err := inv.RequestsNamed(ctx, p.Module) if err != nil { say("the build requests for %s could not be read: %v", p.Module, err) @@ -744,3 +795,46 @@ func withdrawPending(ctx context.Context, inv *inventory.Inventory, node, module } return "", false, nil } + +// keyPendingEnded is why a pending assignment waiting for a module's key ends: the indexer no longer holds the module, +// or the builder's key of its fingerprint failed; empty while it is coming. A variable so a test needs no bus. +var keyPendingEnded = func(ctx context.Context, module string) (string, error) { + keys, closeKeys, err := openBuilderKeys() + if err != nil { + return "", err + } + defer closeKeys() + v, err := keys.ModuleAt(ctx, module, "") + if errors.Is(err, builderkeys.ErrNoValue) { + return fmt.Sprintf("the indexer no longer holds %s", module), nil + } + if err != nil || v.FP2 == "" { + return "", err + } + k, err := keys.Build(ctx, module, v.FP2) + if err != nil { + return "", err + } + if k.Failed() { + return k.Says(), nil + } + return "", nil +} + +// settlePendingOnKey assigns, now, every pending assignment of a module its done key just registered (ADR 0501 §6): +// what an outcome of a build did for one waiting on that build. +func settlePendingOnKey(ctx context.Context, open *stores, module string) []string { + waiting, err := open.inventory.Pending(ctx, time.Time{}) + if err != nil { + return []string{fmt.Sprintf("the pending assignments could not be read after %s was registered: %v", module, err)} + } + var said []string + for _, p := range waiting { + if p.Module == module { + if line := applyPending(ctx, open, p, keyPendingPrefix+module); line != "" { + said = append(said, line) + } + } + } + return said +} diff --git a/cmd/mesh-controller/pending_integration_test.go b/cmd/mesh-controller/pending_integration_test.go index 7000200a..e45d5207 100644 --- a/cmd/mesh-controller/pending_integration_test.go +++ b/cmd/mesh-controller/pending_integration_test.go @@ -13,6 +13,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -36,11 +37,13 @@ func aCatalogueMesh(t *testing.T) *stores { return open } +// mergeAdding keeps the build request a merge adding a module asked before R4e (issue 300): nothing asks one since (ADR +// 0501 §6), and the pending assignments still read the ones kept. These tests are of what an assignment does with one. func mergeAdding(t *testing.T, open *stores, dir, commit string) { t.Helper() - m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: commit, - Paths: []string{dir + "/module.json"}, ModuleDirs: []string{dir}, ModuleDirsSaid: true} - if err := (following{open: open}).SourceMoved(t.Context(), m); err != nil { + if err := open.inventory.RecordBuildRequest(t.Context(), inventory.BuildRequest{ID: "b-" + dir, + Repository: "novox/mesh-catalog", Seat: "git", Path: dir, Ref: "main", Commit: commit, For: "merge", + At: time.Now().UTC()}); err != nil { t.Fatal(err) } } @@ -225,101 +228,10 @@ func TestTheConditionClearsWhenTheModuleIsAssignedLater(t *testing.T) { } } -func TestAssignWithBuildAsksForAKnownModule(t *testing.T) { - open := aCatalogueMesh(t) - ctx := t.Context() - inv := open.inventory - if err := inv.RecordBuildRequest(ctx, inventory.BuildRequest{ID: "build-old", Repository: "novox/mesh-catalog", - Seat: "git", Path: "modules/sensors", Ref: "main", For: "build", At: time.Now().Add(-time.Hour)}); err != nil { - t.Fatal(err) - } - if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-old", Repository: "novox/mesh-catalog", Ref: "main", - Path: "modules/sensors", On: "anchor", Failed: "no space left on device"}); err != nil { - t.Fatal(err) - } - asked := asksWithPaths(t) - said, err := assignWith(ctx, open, "laptop", assignOptions{Build: true}, "sensors") - if err != nil { - t.Fatal(err) - } - if len(*asked) != 1 || (*asked)[0] != [3]string{"novox/mesh-catalog", "modules/sensors", "main"} { - t.Fatalf("asked %v", *asked) - } - for _, want := range []string{"known and not registered", "no space left on device", - "build b-modules/sensors of novox/mesh-catalog@main (modules/sensors) is asked for now", "kept as pending"} { - if !strings.Contains(said, want) { - t.Fatalf("the answer does not say %q:\n%s", want, said) - } - } - pending := pendingOf(t, open) - if len(pending) != 1 || pending[0].Build != "b-modules/sensors" { - t.Fatalf("pending: %+v", pending) - } - requests, _ := inv.RequestsNamed(ctx, "sensors") - if len(requests) != 2 || requests[0].ID != "b-modules/sensors" || requests[0].For != "assign" { - t.Fatalf("the request is not kept as assign's: %+v", requests) - } -} - -func TestAssignWithBuildRepointsTheWaitingAssignment(t *testing.T) { - open := aCatalogueMesh(t) - ctx := t.Context() - inv := open.inventory - if err := inv.RecordBuildRequest(ctx, inventory.BuildRequest{ID: "build-old", Repository: "novox/mesh-catalog", - Seat: "git", Path: "modules/sensors", Ref: "main", For: "merge", At: time.Now().Add(-time.Hour)}); err != nil { - t.Fatal(err) - } - if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-old", Repository: "novox/mesh-catalog", Ref: "main", - Path: "modules/sensors", On: "anchor", Failed: "boom"}); err != nil { - t.Fatal(err) - } - // Waiting on the failed build, its expiry not yet settled. - if _, err := inv.RecordPending(ctx, inventory.PendingAssignment{Node: "laptop", Module: "sensors", - Build: "build-old", Repository: "novox/mesh-catalog", Path: "modules/sensors"}); err != nil { - t.Fatal(err) - } - _, err := assign(ctx, open, "laptop", "sensors") - if err == nil || !strings.Contains(err.Error(), "already has a pending assignment of sensors, waiting for build build-old") { - t.Fatalf("known and not built with a pending assignment waiting: %v", err) - } - asked := asksWithPaths(t) - said, err := assignWith(ctx, open, "laptop", assignOptions{Build: true}, "sensors") - if err != nil || !strings.Contains(said, "which waited for build build-old, now waits for build b-modules/sensors") { - t.Fatalf("assign with build: %q %v", said, err) - } - if len(*asked) != 1 { - t.Fatalf("asked %v", *asked) - } - pending := pendingOf(t, open) - if len(pending) != 1 || pending[0].Build != "b-modules/sensors" || pending[0].State != inventory.PendingWaiting { - t.Fatalf("pending: %+v", pending) - } - // The new build in flight now: build "true" again asks nothing. - if said, err := assignWith(ctx, open, "laptop", assignOptions{Build: true}, "sensors"); err != nil || - !strings.Contains(said, "no second build was asked") || len(*asked) != 1 { - t.Fatalf("a second build true while the build runs: %q %v, asked %v", said, err, *asked) - } -} - func TestAFailedAskIsNotABuildInFlight(t *testing.T) { open := aCatalogueMesh(t) ctx := t.Context() inv := open.inventory - asksOfEveryModule(t) // the one ask left is the builder's own (ADR 0453 §5): this test is of what a walk does around it - was := askABuild - askABuild = func(context.Context, buildSource, string, string) (string, error) { - return "", errors.New("cannot submit a build: nats: timeout") - } - t.Cleanup(func() { askABuild = was }) - mergeAdding(t, open, "modules/sensors", "3da80a4b00aa") - _, err := assign(ctx, open, "laptop", "sensors") - if err == nil || !strings.Contains(err.Error(), "the merge that added it (3da80a4b) could not ask for its build: cannot submit") { - t.Fatalf("a merge that could not ask: %v", err) - } - if len(pendingOf(t, open)) != 0 { - t.Fatal("a pending assignment waits on a build never asked") - } - if err := inv.RecordBuildRequest(ctx, inventory.BuildRequest{ID: "build-waited", Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/gauges", Ref: "main", For: "build"}); err != nil { t.Fatal(err) @@ -327,7 +239,7 @@ func TestAFailedAskIsNotABuildInFlight(t *testing.T) { if err := inv.MarkNotAsked(ctx, "build-waited", "cannot submit a build: no responders"); err != nil { t.Fatal(err) } - _, err = assign(ctx, open, "laptop", "gauges") + _, err := assign(ctx, open, "laptop", "gauges") if err == nil || !strings.Contains(err.Error(), "build build-waited, asked by build, was not handed over") { t.Fatalf("a build not handed over: %v", err) } @@ -737,3 +649,31 @@ func TestABuildNoLongerWaitedForIsStillInFlight(t *testing.T) { t.Fatalf("past the bound: %+v", rows) } } + +// A10 (ADR 0501 §6; issue 1851): an assignment of a module the indexer holds and the catalogue does not yet waits for +// the builder's done key of it, asking nothing, and is made when that key registers the module. +func TestAnAssignmentWaitsForTheKeyAndAsksNothing(t *testing.T) { + open := aCatalogueMesh(t) + ctx := t.Context() + noBuildAsked(t) + b := aBuilderOnTheBus(t) + fp := fpOf('a') + manifest := b.indexed(t, "sensors", "1", "novox/mesh-catalog", "modules/sensors", "3da80a4b00aa0000", fp) + b.built(t, "sensors", "1", fp, manifest, sdkdelivery.BuildBuilding) + said, err := assignWith(ctx, open, "laptop", assignOptions{Build: true}, "sensors") + if err != nil || !strings.Contains(said, "registered from the builder's done key") || !strings.Contains(said, "kept as pending") { + t.Fatalf("A10: the assignment of a module whose key is coming: %q %v", said, err) + } + if p := pendingOf(t, open); len(p) != 1 || p[0].Build != keyPendingPrefix+"sensors" { + t.Fatalf("A10: pending %+v", p) + } + settlePending(ctx, open, time.Now().Add(time.Hour)) + if p := pendingOf(t, open); p[0].State != inventory.PendingWaiting { + t.Fatalf("A10: a pending assignment waiting for a building key expired: %+v", p) + } + b.built(t, "sensors", "1", fp, manifest, sdkdelivery.BuildDone) + takeNewModules(ctx, open, "sensors") + if !slices.Contains(assignedTo(t, open, "laptop"), "sensors") { + t.Fatalf("A10: the key registered sensors and the pending assignment was not made: %+v", pendingOf(t, open)) + } +} diff --git a/cmd/mesh-controller/pending_keys_test.go b/cmd/mesh-controller/pending_keys_test.go new file mode 100644 index 00000000..4423f0be --- /dev/null +++ b/cmd/mesh-controller/pending_keys_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + "github.com/novox/mesh-controller/internal/inventory" +) + +// A10 (ADR 0501 §6; issue 1851): a module the catalogue does not hold and the indexer does is one whose key is coming: +// an assignment of it waits for that key, named by the module, with no build asked and no build request read. +func TestAModuleTheIndexerHoldsIsWaitedForByItsKey(t *testing.T) { + was := indexedModule + indexedModule = func(_ context.Context, module string) (sdkdelivery.Module, bool, error) { + return sdkdelivery.Module{Repository: "novox/mesh-catalog", Commit: "3da80a4b00aa", Path: "modules/" + module}, true, nil + } + t.Cleanup(func() { indexedModule = was }) + where, err := whereIs(t.Context(), noRequests{}, "sensors", time.Now()) + if err != nil { + t.Fatal(err) + } + if where.kind != keyComing || where.request.ID != keyPendingPrefix+"sensors" || where.request.Path != "modules/sensors" || + !strings.Contains(where.said, "registered from the builder's done key") { + t.Fatalf("a module whose key is coming is read as %+v", where) + } +} + +// noRequests is records with no build request and no module: what a mesh after R4e reads of a new module. +type noRequests struct{} + +func (noRequests) RequestsNamed(context.Context, string) ([]inventory.RequestOutcome, error) { + return nil, nil +} +func (noRequests) Catalogue(context.Context) (map[string]catalogue.Manifest, error) { return nil, nil } +func (noRequests) RequestedNames(context.Context) ([]string, error) { return nil, nil }