diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go index e8a933db..bf0e9bff 100644 --- a/cmd/mesh-controller/compose_sdk.go +++ b/cmd/mesh-controller/compose_sdk.go @@ -11,6 +11,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/inventory" ) // The controller's composition on mesh-sdk's (novox/hq ADR 0453 clause 3, design 47 R4b, issues 1386 to 1388): every @@ -34,6 +35,12 @@ func withInputCache(ctx context.Context) context.Context { return context.WithValue(ctx, inputCacheKey{}, &inputCache{by: map[string]*meshInput{}}) } +// freshInput is ctx for one composition whose Input is read afresh and kept only until the composition makes something: a +// send that is composed after another one made and recorded things (a push's machines, one after the other). +func freshInput(ctx context.Context) context.Context { + return context.WithValue(ctx, inputCacheKey{}, &inputCache{by: map[string]*meshInput{}}) +} + // keptSignature is what the Input read depends on in ctx: which recorded modules each machine keeps at the build it runs // (recorded_kept.go), so an Input read for a person's push is never taken for one read for a gated send. func keptSignature(ctx context.Context) string { @@ -105,6 +112,43 @@ func generatorsOf(ctx context.Context, open *stores) (map[string]catalogue.Gener return compose.Generators(in.Input) } +// whoResolvesOn is the machines whose resolution answers a requirement, and why the others did not (whoResolves over the +// Input): by what a module provides, not by its name. A machine whose set does not compose is in refused, said; one the mesh +// could not be asked about is an error, never a refusal (novox/hq 04-ISSUES/152). +func whoResolvesOn(ctx context.Context, open *stores, requirement string) (map[string]bool, map[string]string, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, nil, err + } + on := map[string]bool{} + refused := map[string]string{} + for _, n := range in.sortedNodes() { + resolved, err := compose.Resolve(in.Input, n.Name) + switch { + case compose.IsUnresolvable(err): + refused[n.Name] = err.Error() + continue + case err != nil: + return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err) + } + for _, m := range resolved.Plan.Modules { + for _, offered := range m.Offers() { + if offered == requirement { + on[n.Name] = true + } + } + } + } + return on, refused, nil +} + +// sortedNodes is the Input's nodes by name, the order the controller reads them in. +func (in *meshInput) sortedNodes() []compose.Node { + out := append([]compose.Node(nil), in.Nodes...) + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out +} + // composeQuestion composes a node's declaration as a question asks it: nothing is made, and what a send would make is a // stand-in (Foreseeing) or left out (Reading). It also answers what the node-engine's validator refuses of it. The lines the // composition found are said where the controller says its own (standard error, or the writer an act gave). @@ -140,19 +184,26 @@ func markUnresolvable(err error) error { // then what it named foreseen is made (perform), the stores are read again and it is composed again. A declaration that // still names anything foreseen is refused, said, and never sent. func composeSend(ctx context.Context, open *stores, node string) (sendable, error) { + declared, _, err := composeSendValidated(ctx, open, node) + return declared, err +} + +// composeSendValidated is composeSend and what the node-engine's validator refuses of the declaration it composed, for the +// merge gate's two passes (composedAndValidated in its Allocating mode). +func composeSendValidated(ctx context.Context, open *stores, node string) (sendable, []string, error) { for attempt := 0; ; attempt++ { in, err := inputFor(ctx, open) if err != nil { - return sendable{}, err + return sendable{}, nil, err } - d, _, err := compose.ComposeIn(in.Input, node, compose.Foreseeing) + d, problems, err := compose.ComposeIn(in.Input, node, compose.Foreseeing) if err != nil { - return sendable{}, markUnresolvable(err) + return sendable{}, nil, markUnresolvable(err) } left := stillForeseen(d) resolved, rerr := compose.Resolve(in.Input, node) if rerr != nil { - return sendable{}, markUnresolvable(rerr) + return sendable{}, nil, markUnresolvable(rerr) } if len(resolved.Foreseen) > 0 { left = append(left, resolved.Foreseen...) @@ -167,23 +218,67 @@ func composeSend(ctx context.Context, open *stores, node string) (sendable, erro out.Bindings = boundToData(resolved.Plan, d.LeftOutWhy) if len(d.Filled) > 0 { if out.plans, err = handedPlans(in, node, resolved); err != nil { - return sendable{}, err + return sendable{}, nil, err } } - return out, nil + return out, problems, nil } if attempt > 0 { - return sendable{}, fmt.Errorf("%s was composed twice and still names what only a send makes (%s): nothing is sent "+ + return sendable{}, nil, fmt.Errorf("%s was composed twice and still names what only a send makes (%s): nothing is sent "+ "that stands in for a value", node, strings.Join(left, "; ")) } if _, err := perform(ctx, open, in, node, d); err != nil { - return sendable{}, err + return sendable{}, nil, err } // What was made is in the stores now: the next read is a fresh one. forgetInput(ctx) } } +// restOfTheMesh is what every other machine holds and offers (theRestOfTheMesh over the Input), for a question: the lines it +// finds are said where the controller says its own, and a seat it would pass on is passed by the next send (perform), not by +// the question. shelf, when given, replaces the Input's catalogue: a caller composing a change's shelf. +func restOfTheMesh(ctx context.Context, open *stores, shelf map[string]catalogue.Manifest, exclude string) (catalogue.World, error) { + in, err := inputFor(ctx, open) + if err != nil { + return catalogue.World{}, err + } + input := in.Input + if shelf != nil { + input.Catalogue = shelf + } + world, passes, left, err := compose.TheRestOfTheMesh(input, exclude) + if err != nil { + return catalogue.World{}, err + } + var w io.Writer = os.Stderr + if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok { + w = lw + } + for _, l := range left { + fmt.Fprintln(w, l.String()) + } + for _, line := range passes.Said { + fmt.Fprintln(w, line) + } + return world, nil +} + +// restOfTheMeshOf is restOfTheMesh for a caller that holds the inventory alone: the other stores the Input reads are opened for +// the call and let go of again. +func restOfTheMeshOf(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest, exclude string) (catalogue.World, error) { + open := &stores{inventory: inv} + defer func() { + if open.licences != nil { + open.licences.Close() + } + if open.identity != nil { + open.identity.Close() + } + }() + return restOfTheMesh(ctx, open, shelf, exclude) +} + // sayFound says the lines a composition found while composing, once each: a machine left out of the rest of the mesh, a seat // that two claimants could take, the bus's users left out for want of a credential. func sayFound(ctx context.Context, d compose.Declared) { diff --git a/cmd/mesh-controller/held_back.go b/cmd/mesh-controller/held_back.go index fd31c63e..e02c8d34 100644 --- a/cmd/mesh-controller/held_back.go +++ b/cmd/mesh-controller/held_back.go @@ -107,6 +107,8 @@ func buildName(commit string) string { // (heldBack), and answers only the machines held. A machine whose set cannot be worked out is left // to the send, which says why. func heldMachines(ctx context.Context, open *stores, names []string) (map[string][]string, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) out := map[string][]string{} if len(names) == 0 { return out, nil @@ -125,7 +127,7 @@ func heldMachines(ctx context.Context, open *stores, names []string) (map[string return nil, err } for _, node := range names { - plan, _, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { continue } @@ -265,13 +267,17 @@ func flushBehind(ctx context.Context, open *stores, nodes []inventory.Node, hand // is left out of it said, and its declaration allocated. func composeForPush(open *stores, gens map[string]catalogue.Generator) func(held context.Context, node string) (sendable, error) { return func(held context.Context, node string) (sendable, error) { - plan, settings, err := planFor(held, open, node) + // Read afresh for each machine: the send before it made and recorded things. gens is the private network the + // controller's own composition took; the SDK's is computed from the same Input. + _ = gens + held = freshInput(held) + plan, _, err := resolveNode(held, open, node) if err != nil { return sendable{}, err } reportUnhostable(node, plan) reportKept(held, plan) - declared, err := declarationWith(held, open, node, plan, settings, gens, Allocating) + declared, err := composeSend(held, open, node) if err == nil { reportLeftOut(node, declared) } diff --git a/cmd/mesh-controller/merge_gate.go b/cmd/mesh-controller/merge_gate.go index 02adf909..b04927c2 100644 --- a/cmd/mesh-controller/merge_gate.go +++ b/cmd/mesh-controller/merge_gate.go @@ -972,7 +972,7 @@ func composeEveryMachine(ctx context.Context, in mergeCheckInput, shelf map[stri if err != nil { return nil, nil, notes, err } - gens, gensErr := generators(ctx, open) + gens, gensErr := generatorsOf(ctx, open) out := map[string]mergeComposed{} for pass := 0; pass < 2; pass++ { for _, m := range in.facts.Machines { @@ -981,7 +981,7 @@ func composeEveryMachine(ctx context.Context, in mergeCheckInput, shelf map[stri oneLine(gensErr.Error())}} continue } - declared, problems, err := composedAndValidated(ctx, open, m.Name, gens, Allocating) + declared, problems, err := composeSendValidated(ctx, open, m.Name) if pass == 0 { continue } @@ -1047,7 +1047,7 @@ func tryAssigning(ctx context.Context, open *stores, f snapshot.Facts, m catalog if _, err := open.inventory.Assign(ctx, machine.Name, m.Module); err != nil { return mergeTrial{Machine: machine.Described(), Said: oneLine(err.Error())} } - _, problems, err := composedAndValidated(ctx, open, machine.Name, gens, Allocating) + _, problems, err := composeSendValidated(ctx, open, machine.Name) _ = open.inventory.Unassign(ctx, machine.Name, m.Module) t := mergeTrial{Machine: machine.Described()} if err != nil { diff --git a/cmd/mesh-controller/plan.go b/cmd/mesh-controller/plan.go index 3f51ec6a..40e34796 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -424,11 +424,7 @@ func sayLeftOut(ctx context.Context, node string, err error) { // `plan --json` handed to anything reading it. func declarationFor(ctx context.Context, open *stores, node string, plan catalogue.Resolution, settings catalogue.SettingsBy) (sendable, error) { - gens, err := generators(ctx, open) - if err != nil { - return sendable{}, err - } - // **Allocating, because `plan` is the send without the sending.** It is one machine, named by + // **A send, because `plan` is the send without the sending.** It is one machine, named by // a person, who is asking what a push would do โ€” so the port it shows and the secret it seals // have to be the ones a push would use, and both are kept once chosen. Showing numbers that a // later push would replace would make the command answer a question nobody asked. @@ -436,7 +432,7 @@ func declarationFor(ctx context.Context, open *stores, node string, // The line is not "a question may not write". It is who is asking and how often: this is a // person, about one machine, on purpose. What may not write is the comparison the mesh runs // over every machine to answer whether each is up to date โ€” see Choosing. - return declarationWith(ctx, open, node, plan, settings, gens, Allocating) + return composeSend(ctx, open, node) } // declarationWith is the same, for a caller that has already worked out the generators once and @@ -1260,7 +1256,7 @@ func zonesInTheMesh(ctx context.Context, open *stores) ([]catalogue.ZoneAt, erro var zones []catalogue.ZoneAt var public []string for _, n := range nodes { - plan, _, err := planFor(ctx, open, n.Name) + plan, _, err := resolveNode(ctx, open, n.Name) switch { case unresolvable(err): continue @@ -1503,6 +1499,8 @@ func listensLines(m catalogue.Manifest) []string { } func planCommand(ctx context.Context, args []string) error { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) set := flag.NewFlagSet("plan", flag.ContinueOnError) // Because "one resource" does not tell you whether the settings landed. Being able to read // the file before it is sent is the difference between believing a merge worked and knowing. @@ -1528,7 +1526,7 @@ func planCommand(ctx context.Context, args []string) error { } defer open.Close() - plan, settings, err := planFor(ctx, open, args[0]) + plan, settings, err := resolveNode(ctx, open, args[0]) if err != nil { return err } diff --git a/cmd/mesh-controller/push.go b/cmd/mesh-controller/push.go index e4d55169..3a29e07d 100644 --- a/cmd/mesh-controller/push.go +++ b/cmd/mesh-controller/push.go @@ -18,6 +18,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/inventory" @@ -521,7 +522,7 @@ func pushCommand(ctx context.Context, args []string) error { return nil } } - gens, err := generators(ctx, open) + gens, err := generatorsOf(ctx, open) if err != nil { return err } @@ -652,7 +653,9 @@ func pushCommand(ctx context.Context, args []string) error { // for a machine named, as a count for each of many โ€” the full list is `status`'s. unheld := map[string][]catalogue.Unheld{} sending, refusals := composeEach(asked, allotting(held, inv), func(node string) (sendable, error) { - plan, settings, err := planFor(held, open, node) + // What this machine's send reads is read afresh: the send before it made and recorded things. + held := freshInput(held) + plan, _, err := resolveNode(held, open, node) if err != nil { return sendable{}, err } @@ -665,7 +668,7 @@ func pushCommand(ctx context.Context, args []string) error { // The private network is in here with everything else. It used to be composed separately // and prepended, which meant every machine with an address was on it and no machine could // be kept off. It is a module now, so it arrives the way a module does. - declared, err := declarationWith(held, open, node, plan, settings, gens, Allocating) + declared, err := composeSend(held, open, node) if err == nil { reportLeftOut(node, declared) } @@ -1152,7 +1155,7 @@ func brokerListState(ctx context.Context, open *stores, names []string) (busList // refuses rather than sending code whose grants the bus may not hold. It said "not behind" until then: a // send went ahead as if the bus held a list nobody could compose. list, composeErr := "", error(nil) - plan, _, err := planFor(ctx, open, h.Node) + plan, _, err := resolveNode(ctx, open, h.Node) if err != nil { composeErr = err } else if list, _, err = busUserList(ctx, inv, plan.Modules); err != nil { @@ -1229,8 +1232,8 @@ func sendToEach(ctx context.Context, open *stores, names []string) ([]string, er } defer ident.Close() - gens, err := generators(ctx, open) - if err != nil { + // The private network every declaration of the send is composed with: not computable refuses the send up front. + if _, err := generatorsOf(ctx, open); err != nil { return nil, err } holder, behind, err := brokerBehind(ctx, open, names) @@ -1272,14 +1275,16 @@ func sendToEach(ctx context.Context, open *stores, names []string) ([]string, er refusals = append(refusals, fmt.Sprintf("%s:\n%v", name, err)) continue } - plan, settings, err := planFor(ctx, open, name) + // Read afresh for each machine: the send before it made and recorded things. + composing := freshInput(ctx) + plan, _, err := resolveNode(composing, open, name) if err != nil { refusals = append(refusals, fmt.Sprintf("%s:\n%v", name, err)) continue } reportUnhostable(name, plan) reportKept(ctx, plan) - declared, err := declarationWith(ctx, open, name, plan, settings, gens, Allocating) + declared, err := composeSend(composing, open, name) if err != nil { refusals = append(refusals, fmt.Sprintf("%s:\n%v", name, err)) continue @@ -1454,20 +1459,21 @@ type planned struct { func wouldSendFrom(ctx context.Context, open *stores, nodes []inventory.Node, plans map[string]planned) (map[string]string, error) { - gens, err := generators(ctx, open) - if err != nil { + if _, err := generatorsOf(ctx, open); err != nil { return nil, err } + var err error out := map[string]string{} for _, n := range nodes { known, have := plans[n.Name] plan, settings := known.plan, known.settings if !have { - if plan, settings, err = planFor(ctx, open, n.Name); err != nil { + if plan, settings, err = resolveNode(ctx, open, n.Name); err != nil { continue } } - declared, err := declarationWith(ctx, open, n.Name, plan, settings, gens, Reading) + _, _ = plan, settings + declared, _, err := composeQuestion(ctx, open, n.Name, compose.Reading, false) if err != nil { continue } diff --git a/cmd/mesh-controller/settings_walk.go b/cmd/mesh-controller/settings_walk.go index 68862177..08fd5aa4 100644 --- a/cmd/mesh-controller/settings_walk.go +++ b/cmd/mesh-controller/settings_walk.go @@ -10,6 +10,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" "github.com/novox/mesh-controller/internal/inventory" ) @@ -97,15 +98,14 @@ var openSettingsWalk = func(ctx context.Context, open *stores, s inventory.PlanS // that send composes it, every build kept, and compared with what the machine was last sent (ADR 0217 ยง3). var settingsDataMoves = func(ctx context.Context, open *stores, node string) ([]dataMove, error) { keeping := keepingBuildsForSettings(keepingRecorded(ctx), node) - gens, err := generators(keeping, open) - if err != nil { + keeping = withInputCache(keeping) + if _, err := generatorsOf(keeping, open); err != nil { return nil, err } - plan, settings, err := planFor(keeping, open, node) - if err != nil { + if _, _, err := resolveNode(keeping, open, node); err != nil { return nil, err } - declared, err := declarationWith(keeping, open, node, plan, settings, gens, Reading) + declared, _, err := composeQuestion(keeping, open, node, compose.Reading, false) if err != nil { return nil, err }