Keep an assignment of a module whose key is coming pending on that key and ask no build; nothing asks a request again at start (issue 1851, A10)

This commit is contained in:
2026-10-22 19:05:04 +02:00
parent 9da79ef8c3
commit 0aec975d19
7 changed files with 222 additions and 166 deletions
@@ -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
+2 -44
View File
@@ -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"
+8
View File
@@ -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)
+15 -5
View File
@@ -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.
+116 -22
View File
@@ -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
}
+35 -95
View File
@@ -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))
}
}
+39
View File
@@ -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 }