Send over mesh-sdk's composition: push, the pushes of plans and rollouts, held back, plan and the merge gate's passes (issue 1386)
Every send composes through composeSend (compose over the Input, perform what it named foreseen, read the stores again, compose again, refuse a declaration that still stands in for a value): push.go's two composing sites, composeForPush, declarationFor (the plan verb, which allocates on purpose for one named machine) and the merge gate's two passes and its trial assignment (composeSendValidated, which answers the node-engine's validator beside it). Each machine of a send reads the mesh afresh (freshInput), because the send before it made and recorded things. None calls declarationWith with Allocating any more. The private network is still computed up front, so a send refuses on it before anything is composed. Also here, as they share the helpers: whoResolvesOn, restOfTheMesh and restOfTheMeshOf in compose_sdk.go, and the planFor callers in held_back.go, plan.go and push.go.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+18
-12
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user