mesh/delivery merged: source.main moved to novox/mesh-sdk@be77f6656326
mesh/delivery-group group fix/1126-shadow-bodies merged: every member is merged
mesh/check pass 362a3f1378aa on 03da552cb331 | unit 2860 run, 1141 kept, 36 skipped | integration 2887 run, 1241 kept, 29 skipped | over bound: unit (…
ace and g14 answered 'nothing in this mesh provides artifact-store … assign distribution to a node' while novox runs it: a module another node cannot run in this composition (catalogue Resolution.Unhostable) offers nothing, silently. The refusal now names each such module and what it lacks, and each node left out. And the culprit search composed the whole mesh once per placed module for a refusal no stub changes (ace's answer took 7m46s): only the modules the refusal names are tried, at most 8.
239 lines
8.9 KiB
Go
239 lines
8.9 KiB
Go
package compose
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// notResolvable marks the one failure in a node's resolution that is a statement about the node: its assigned modules
|
|
// do not compose. Every other failure means the mesh could not be asked. Whoever gathers across the mesh skips a node
|
|
// that cannot resolve and never a node that could not be read (novox/hq 04-ISSUES/152).
|
|
type notResolvable struct{ err error }
|
|
|
|
func (n notResolvable) Error() string { return n.err.Error() }
|
|
func (n notResolvable) Unwrap() error { return n.err }
|
|
|
|
// unresolvable reports whether err is a node's own set failing to compose.
|
|
func unresolvable(err error) bool {
|
|
var n notResolvable
|
|
return errors.As(err, &n)
|
|
}
|
|
|
|
// Passes is what a composition found that the mesh must record, and did not: each seat held once whose holder on record
|
|
// is assigned nowhere that exactly one claimant takes over (novox/hq ADR 0449 clause 4), and the lines that say what was
|
|
// found. The caller records a pass and writes its hand act; this package does neither.
|
|
type Passes struct {
|
|
// Passes are the seats to pass, in the order they were found.
|
|
Passes []catalogue.SeatPass
|
|
// Said are the lines the finding says (a seat two claimants could take, one nothing can).
|
|
Said []string
|
|
}
|
|
|
|
// LeftOut is a node that does not resolve in the view of the rest of the mesh, with why: it offers nothing and holds
|
|
// nothing as far as every other machine's plan can tell (novox/hq issue 188).
|
|
type LeftOut struct {
|
|
Node string
|
|
Err error
|
|
}
|
|
|
|
// String is the line the controller says of it.
|
|
func (l LeftOut) String() string {
|
|
return fmt.Sprintf("%s is left out of the rest of the mesh: it does not resolve: %v", l.Node, l.Err)
|
|
}
|
|
|
|
// TheRestOfTheMesh is what every other node holds and offers, as the controller works it out (plan.go theRestOfTheMesh):
|
|
// two passes over the others, because what a node offers needs it resolved and resolving it may need what the mesh
|
|
// offers. The first takes brokered requirements on trust and answers only what each node offers; the second answers
|
|
// everything with that in hand. Nothing is declared from the first.
|
|
//
|
|
// It writes nothing. A seat the mesh passes on is returned in Passes for the caller to record; the World carries the
|
|
// holdings as they stand after the passes, and Input's own are unchanged. A node left out is returned with its reason, in
|
|
// the order the controller said them (the first pass's, then the second's when no node is excluded).
|
|
func TheRestOfTheMesh(in Input, exclude string) (catalogue.World, Passes, []LeftOut, error) {
|
|
c := newComposer(in)
|
|
return c.theRestOfTheMesh(exclude)
|
|
}
|
|
|
|
func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, []LeftOut, error) {
|
|
shelf := c.in.Catalogue
|
|
holdings := append([]catalogue.Held(nil), c.in.Holdings...)
|
|
nodes := c.in.sorted()
|
|
|
|
onNetwork, err := c.whereEveryoneIs()
|
|
if err != nil {
|
|
return catalogue.World{}, Passes{}, nil, err
|
|
}
|
|
|
|
type candidate struct {
|
|
node catalogue.Node
|
|
assigned []string
|
|
pins map[string]catalogue.Chosen
|
|
}
|
|
var others []candidate
|
|
// Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4): a seat
|
|
// held once whose holder on record is assigned on no machine passes to its only claimant here.
|
|
everyAssigned := map[string][]string{}
|
|
for _, n := range nodes {
|
|
if len(n.Assigned) > 0 {
|
|
everyAssigned[n.Name] = n.Assigned
|
|
}
|
|
}
|
|
var passes Passes
|
|
holdings, passes = passSeatsOn(holdings, assignmentsOf(shelf, everyAssigned))
|
|
for _, n := range nodes {
|
|
if n.Name == exclude {
|
|
continue
|
|
}
|
|
theirs := everyAssigned[n.Name]
|
|
if len(theirs) == 0 {
|
|
continue
|
|
}
|
|
others = append(others, candidate{
|
|
node: catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities,
|
|
At: onNetwork[n.Name], PublicDomain: n.PublicDomain},
|
|
assigned: theirs, pins: n.Pins})
|
|
}
|
|
|
|
var left []LeftOut
|
|
offered := map[string][]catalogue.Provider{}
|
|
var firstHeld []catalogue.Held
|
|
c.unhostable = nil
|
|
for _, o := range others {
|
|
got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
|
|
if err != nil {
|
|
// Said, not skipped: a machine dropped here offers nothing and holds nothing.
|
|
left = append(left, LeftOut{Node: o.node.Name, Err: err})
|
|
continue
|
|
}
|
|
for _, u := range got.Unhostable {
|
|
c.unhostable = append(c.unhostable, fmt.Sprintf("%s on %s lacks %s", u.Module, o.node.Name, strings.Join(u.Missing, ", ")))
|
|
}
|
|
firstHeld = append(firstHeld, got.Claims...)
|
|
for _, m := range got.Modules {
|
|
for _, name := range m.OffersAt(catalogue.ScopeMesh) {
|
|
// What that module says a consumer needs to know, with that node's settings on it.
|
|
serves, layers, err := c.servedOnNode(o.node.Name, m, name)
|
|
if err != nil {
|
|
return catalogue.World{}, Passes{}, nil, err
|
|
}
|
|
// And its own composed names, as that node composes them (novox/hq ADR 0346).
|
|
serves, leftOut, err := got.ServedOwnNames(m, name, serves, catalogue.WithDefaults(m, layers))
|
|
if err != nil {
|
|
return catalogue.World{}, Passes{}, nil, err
|
|
}
|
|
offered[name] = append(offered[name], catalogue.Provider{
|
|
Node: o.node.Name, At: o.node.At, Serves: serves, Module: m.Module, LeftOut: leftOut})
|
|
}
|
|
}
|
|
}
|
|
for k := range offered {
|
|
sort.Slice(offered[k], func(i, j int) bool {
|
|
return offered[k][i].Node < offered[k][j].Node
|
|
})
|
|
}
|
|
|
|
// The second pass is given the first pass's holdings (novox/hq ADR 0110).
|
|
world := catalogue.World{Offered: offered, Held: firstHeld, Holdings: holdings}
|
|
var held []catalogue.Held
|
|
for _, o := range others {
|
|
// Each machine is resolved with its own pins, as its plan is.
|
|
theirs := world
|
|
theirs.Pinned = o.pins
|
|
got, err := catalogue.Resolve(shelf, o.assigned, o.node, theirs)
|
|
if err != nil {
|
|
// Said only for the whole-mesh view: with one machine excluded, the view is partial by design.
|
|
if exclude == "" {
|
|
left = append(left, LeftOut{Node: o.node.Name, Err: err})
|
|
}
|
|
continue
|
|
}
|
|
held = append(held, got.Claims...)
|
|
}
|
|
world.Held = held
|
|
return world, passes, left, nil
|
|
}
|
|
|
|
// passSeatsOn finds, in the composition that finds it, the new holder of each seat held once whose holder on record is
|
|
// assigned nowhere and which has exactly one claimant able to hold it (novox/hq ADR 0449 clause 4), and answers the
|
|
// holdings as they are after it, with the passes. Nothing is recorded.
|
|
func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) ([]catalogue.Held, Passes) {
|
|
passes, said := catalogue.SeatsToPassOn(holdings, assignments)
|
|
out := Passes{Said: said}
|
|
for _, p := range passes {
|
|
out.Passes = append(out.Passes, p)
|
|
var next []catalogue.Held
|
|
for _, h := range holdings {
|
|
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == p.Seat.Name {
|
|
continue
|
|
}
|
|
next = append(next, h)
|
|
}
|
|
p.To.Site = p.From.Site
|
|
holdings = append(next, p.To)
|
|
}
|
|
return holdings, out
|
|
}
|
|
|
|
// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. A module assigned and
|
|
// missing from the shelf is still assigned: an assignment with no claims, so its holder is not taken for gone.
|
|
func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment {
|
|
var out []catalogue.Assignment
|
|
names := make([]string, 0, len(assigned))
|
|
for node := range assigned {
|
|
names = append(names, node)
|
|
}
|
|
sort.Strings(names)
|
|
for _, node := range names {
|
|
for _, module := range assigned[node] {
|
|
m, ok := shelf[module]
|
|
if !ok {
|
|
m = catalogue.Manifest{Module: module}
|
|
}
|
|
out = append(out, catalogue.Assignment{Node: node, Manifest: m})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// servedOnNode is what a module on a node tells a consumer of one of its provisions, with THAT node's ports on it: the
|
|
// port the node was given over the one the mesh assigned over the manifest's own, settled with the node's settings
|
|
// layers. The one derivation for every reader of a provider's address.
|
|
func (c *composer) servedOnNode(node string, m catalogue.Manifest, provision string) (map[string]any, []catalogue.Layer, error) {
|
|
ports, layers, err := c.portsGivenOn(node, m)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
serves := catalogue.ServedOn(m, provision, ports)
|
|
if len(serves) > 0 {
|
|
serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
return serves, layers, nil
|
|
}
|
|
|
|
// portsGivenOn is where a node puts a module's ports — assigned, then given over them — and the node's settings layers
|
|
// for the module.
|
|
func (c *composer) portsGivenOn(node string, m catalogue.Manifest) (map[int]int, []catalogue.Layer, error) {
|
|
n, err := c.in.node(node)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
ports := map[int]int{}
|
|
for wanted, at := range n.Ports[m.Module] {
|
|
ports[wanted] = at
|
|
}
|
|
layers := n.Settings[m.Module]
|
|
if given, err := catalogue.GivenPorts(m, layers); err == nil {
|
|
for wanted, at := range given {
|
|
ports[wanted] = at
|
|
}
|
|
}
|
|
return ports, layers, nil
|
|
}
|