A node's plans took 2.6-13.6 s of CPU on the provisioner: one composition resolved every machine of the mesh some two hundred times (who is on the private network, and the rest of the mesh, worked out again on every look), three compositions did so again each, and the holder composed twice. A composer now works each out once over its unchanging Input; Composing gives Resolve, Render and Generators one composer; PlansHeldTwice composes the wanted and the newest builds over one read of the bus, says where the time went by phase, and the states reader keeps a build's take-in by its content. WatchedStates takes a process's own writes into the copy at once, so a holder reads its own states from it. Measured on a captured novox mesh: 11.7 s to 0.49 s per composition of an unchanged node, plans and provisions identical on all four nodes. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
256 lines
9.5 KiB
Go
256 lines
9.5 KiB
Go
package compose
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"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) {
|
|
r, ok := c.rest[exclude]
|
|
if !ok {
|
|
r.world, r.passes, r.left, r.err = c.theRestOfTheMeshNow(exclude)
|
|
r.unhostable = c.unhostable
|
|
if c.rest == nil {
|
|
c.rest = map[string]restOf{}
|
|
}
|
|
c.rest[exclude] = r
|
|
}
|
|
// Each caller has its own copy of what it may set or add to; the maps and the slices within are only read.
|
|
c.unhostable = slices.Clone(r.unhostable)
|
|
return r.world, r.passes, slices.Clone(r.left), r.err
|
|
}
|
|
|
|
// theRestOfTheMeshNow is theRestOfTheMesh worked out: every other machine's set resolved, twice.
|
|
func (c *composer) theRestOfTheMeshNow(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
|
|
}
|