235 lines
8.8 KiB
Go
235 lines
8.8 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
|
|
shelf map[string]catalogue.Manifest
|
|
}
|
|
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, shelf: c.shelfFor(n)})
|
|
}
|
|
|
|
var left []LeftOut
|
|
offered := map[string][]catalogue.Provider{}
|
|
var firstHeld []catalogue.Held
|
|
for _, o := range others {
|
|
got, err := catalogue.Resolve(o.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(o.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
|
|
}
|