197 lines
7.8 KiB
Go
197 lines
7.8 KiB
Go
// Package assign is the assigner's judgement, as functions over what it read from the bus: the composition check of a
|
|
// change (novox/hq ADR 0446 clauses 1 to 5, ADR 0528 clause 5, issue 1297), the six rules of what each node should
|
|
// run (ADR 0429 §6, ADR 0528 clause 3) and the comparison with what the controller sends (ADR 0528 clause 4). It reads
|
|
// and writes nothing itself: the program hands it what it watched and writes what it answers.
|
|
package assign
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// SDKRepository is the repository of mesh-sdk: a group with a member there changes the composition itself.
|
|
const SDKRepository = "novox/mesh-sdk"
|
|
|
|
// NotJudged names, in a composition's warnings, the failures of ADR 0446 clause 1 the assigner cannot judge from the
|
|
// bus, because what they need is only in the controller's facts snapshot, which is not on the bus yet (issue 1461;
|
|
// the design limit of R4g, issue 1297): said on every verdict, never passed silently.
|
|
const NotJudged = "not judged until the facts snapshot is on the bus (issue 1461): a consumer left out of a grant, a " +
|
|
"credential left bound elsewhere, a stored setting the changed definition cannot keep, and a new module the " +
|
|
"node-engine's validator would refuse"
|
|
|
|
// Mesh is the mesh as the trunk holds it: every module's manifest by name, the manifests that cannot be read with
|
|
// why, and each node's open assignments.
|
|
type Mesh struct {
|
|
Manifests map[string]catalogue.Manifest
|
|
Unreadable map[string]string
|
|
Assigned map[string][]string
|
|
}
|
|
|
|
// Change is a group's change as the indexer's candidate holds it: the manifest of every module it moves or adds, those
|
|
// it cannot read and why, the modules it removes, every module defined twice with where, the heads of a mesh-sdk
|
|
// member (empty when none), and how many modules it rebuilds.
|
|
type Change struct {
|
|
Group, Heads string
|
|
Manifests map[string]catalogue.Manifest
|
|
Unreadable map[string]string
|
|
Removed []string
|
|
Twice map[string][]string
|
|
SDKHead string
|
|
Rebuilt int
|
|
}
|
|
|
|
// Touched is the machines a change touches (ADR 0446 clause 3): those running a module the change moves, adds or
|
|
// removes, those running a consumer of a provision such a module provides (before or after), and those running a
|
|
// provider of a provision such a module requires after the change. A module the change adds that is assigned nowhere
|
|
// touches no machine.
|
|
func Touched(m Mesh, c Change) []string {
|
|
moved := map[string]bool{}
|
|
for name := range c.Manifests {
|
|
moved[name] = true
|
|
}
|
|
for name := range c.Unreadable {
|
|
moved[name] = true
|
|
}
|
|
for _, name := range c.Removed {
|
|
moved[name] = true
|
|
}
|
|
provided, required := map[string]bool{}, map[string]bool{}
|
|
for name := range moved {
|
|
for _, man := range []catalogue.Manifest{m.Manifests[name], c.Manifests[name]} {
|
|
for _, o := range man.Provides {
|
|
provided[o.Name] = true
|
|
}
|
|
}
|
|
for _, r := range c.Manifests[name].Requires {
|
|
required[r] = true
|
|
}
|
|
}
|
|
var out []string
|
|
for node, modules := range m.Assigned {
|
|
for _, mod := range modules {
|
|
man := m.Manifests[mod]
|
|
if moved[mod] || slices.ContainsFunc(man.Requires, func(r string) bool { return provided[r] }) ||
|
|
slices.ContainsFunc(man.Provides, func(o catalogue.Offer) bool { return required[o.Name] }) {
|
|
out = append(out, node)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// input is the composition's input for the mesh's catalogue and assignments: the nodes by name with what is assigned
|
|
// to them, and every seat the assigned modules claim as held there. What the bus does not carry (places on the
|
|
// private network, settings, credentials, licences) is left empty, so a trunk composition and a change's are made
|
|
// with the same gaps and compared (ADR 0446 clause 1: what was already broken is said and fails nothing).
|
|
func input(catalog map[string]catalogue.Manifest, assigned map[string][]string) compose.Input {
|
|
in := compose.Input{Catalogue: catalog}
|
|
nodes := make([]string, 0, len(assigned))
|
|
for n := range assigned {
|
|
nodes = append(nodes, n)
|
|
}
|
|
sort.Strings(nodes)
|
|
for _, n := range nodes {
|
|
in.Nodes = append(in.Nodes, compose.Node{Name: n, Assigned: slices.Clone(assigned[n])})
|
|
for _, mod := range assigned[n] {
|
|
for _, cl := range catalog[mod].Claims {
|
|
scope := cl.Scope
|
|
if seat, ok := catalogue.SeatNamed(cl.Name); ok && scope == "" {
|
|
scope = seat.Scope
|
|
}
|
|
in.Holdings = append(in.Holdings, catalogue.Held{Claim: cl.Name, Scope: scope, Node: n, Module: mod})
|
|
}
|
|
}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// Compose is the composition check of change c over the mesh m, composed with the mesh-sdk version sdk: every touched
|
|
// machine resolved as the trunk holds it and as the change leaves it. It fails on a manifest of the change that cannot
|
|
// be read, a module a machine runs that the change removes, a module defined twice, and a machine that resolves on the
|
|
// trunk and not with the change; a machine that does not resolve on either is said and fails nothing. It warns past
|
|
// CompositionRebuildWarning modules rebuilt, says what it does not judge (NotJudged), and for a group with a mesh-sdk
|
|
// member says it composed with its own mesh-sdk, not the member's head (issue 1621). took is how long it took.
|
|
func Compose(m Mesh, c Change, sdk string, took time.Duration) delivery.CompositionVerdict {
|
|
v := delivery.CompositionVerdict{Group: c.Group, Heads: c.Heads, SDK: sdk, Took: took.Milliseconds()}
|
|
var fails []string
|
|
failAt := func(machine, words string) {
|
|
if v.Machine == "" && machine != "" {
|
|
v.Machine = machine
|
|
}
|
|
fails = append(fails, words)
|
|
}
|
|
for _, name := range sortedKeys(c.Unreadable) {
|
|
failAt("", fmt.Sprintf("the manifest of %s cannot be read: %s", name, c.Unreadable[name]))
|
|
}
|
|
for _, name := range sortedKeys(c.Twice) {
|
|
failAt("", fmt.Sprintf("%s is defined twice, in %s", name, strings.Join(c.Twice[name], " and ")))
|
|
}
|
|
machines := Touched(m, c)
|
|
v.Machines = machines
|
|
for _, node := range machines {
|
|
for _, name := range c.Removed {
|
|
if slices.Contains(m.Assigned[node], name) {
|
|
failAt(node, fmt.Sprintf("%s runs %s, which the change removes from its source", node, name))
|
|
}
|
|
}
|
|
}
|
|
trunk := input(m.Manifests, m.Assigned)
|
|
changed := map[string]catalogue.Manifest{}
|
|
for name, man := range m.Manifests {
|
|
changed[name] = man
|
|
}
|
|
for name, man := range c.Manifests {
|
|
changed[name] = man
|
|
}
|
|
for _, name := range c.Removed {
|
|
delete(changed, name)
|
|
}
|
|
after := input(changed, m.Assigned)
|
|
for _, node := range machines {
|
|
_, before := compose.Resolve(trunk, node)
|
|
_, now := compose.Resolve(after, node)
|
|
switch {
|
|
case now == nil:
|
|
case before != nil:
|
|
v.Warnings = append(v.Warnings, fmt.Sprintf("%s does not compose on the trunk either, so the change is not blamed: %v", node, before))
|
|
default:
|
|
failAt(node, fmt.Sprintf("%s composes on the trunk and not with the change: %v", node, now))
|
|
}
|
|
}
|
|
if c.Rebuilt > delivery.CompositionRebuildWarning {
|
|
v.Warnings = append(v.Warnings, fmt.Sprintf("the merge rebuilds %d modules, more than %d", c.Rebuilt, delivery.CompositionRebuildWarning))
|
|
}
|
|
if c.SDKHead != "" {
|
|
v.Warnings = append(v.Warnings, fmt.Sprintf("composed with the assigner's own mesh-sdk %s, not the group's mesh-sdk head %s, "+
|
|
"and a field the running controller cannot read is not judged (issues 1621, 1622, 1623)", sdk, c.SDKHead))
|
|
}
|
|
v.Warnings = append(v.Warnings, NotJudged)
|
|
if len(machines) == 0 {
|
|
v.Warnings = append(v.Warnings, "the change touches no machine: no module it moves is assigned anywhere")
|
|
}
|
|
if len(fails) > 0 {
|
|
v.State, v.Words = delivery.CompositionFail, strings.Join(fails, "; ")
|
|
return v
|
|
}
|
|
v.State = delivery.CompositionPass
|
|
return v
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|