Files
mesh-sdk/go/compose/rest.go
T

234 lines
8.7 KiB
Go

package compose
import (
"errors"
"fmt"
"sort"
"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
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
}
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
}