// 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 // Digests is every trunk module's manifest digest in the indexer's manifests bucket, readable or not: what a // composer at another mesh-sdk reads (issue 1622). Digests 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 // Digests is the manifest digest of every module the change moves or adds, readable by the assigner or not, and // Members the head of each member by repository: what a composer at the group's mesh-sdk reads (issues 1622, 1623). Digests map[string]string Members map[string]string } // 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.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)) } return finish(v, m, c, machines, fails) } // finish adds what every composition says beside its own judgement (more modules rebuilt than CompositionRebuildWarning, // what is not judged, the machines not composed, a change that touches none) and its state from fails. func finish(v delivery.CompositionVerdict, m Mesh, c Change, machines []string, fails []string) delivery.CompositionVerdict { if c.Rebuilt > delivery.CompositionRebuildWarning { v.Warnings = append(v.Warnings, fmt.Sprintf("the merge rebuilds %d modules, more than %d", c.Rebuilt, delivery.CompositionRebuildWarning)) } v.Warnings = append(v.Warnings, NotJudged) if could := CouldRun(m, c, machines); len(could) > 0 { v.Warnings = append(v.Warnings, "not judged until the facts snapshot is on the bus (issue 1461, ADR 0537 clause 1): "+ "the machines that could run a moved or added module and are not assigned it: "+strings.Join(could, ", ")) } if grants := GrantsAlone(m, c, machines); len(grants) > 0 { v.Warnings = append(v.Warnings, "not judged until the facts snapshot is on the bus (issue 1461, ADR 0537 clause 1): "+ "the machines whose grants alone the change alters: "+strings.Join(grants, ", ")) } 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 } // CouldRun is, by name, every machine of the mesh not composed that is not assigned a module the change moves or adds // (ADR 0537 clause 1): which of them could run it, the bus does not say (capabilities, settings and places are only in // the facts snapshot), so each is named as not judged. func CouldRun(m Mesh, c Change, composed []string) []string { var out []string for node, modules := range m.Assigned { if slices.Contains(composed, node) { continue } for name := range c.Manifests { if !slices.Contains(modules, name) { out = append(out, node) break } } } sort.Strings(out) return out } // grantsOf is what of a manifest makes grants: its bus declarations and the provisions it requires and offers. func grantsOf(man catalogue.Manifest) string { var offers []string for _, o := range man.Provides { offers = append(offers, o.Name) } return fmt.Sprint(man.Requires, offers, man.Reads, man.State, man.Emits, man.Consumes, man.Invokes, man.Claims, man.Grants) } // GrantsAlone is, by name, every machine not composed whose grants alone the change alters (ADR 0537 clause 1): for a // moved module whose grants differ from the trunk's, the machines assigned the bus's holder (whose user list carries // every module's bus grants) and those assigned a provider of a provision it requires, before or after. func GrantsAlone(m Mesh, c Change, composed []string) []string { provisions := map[string]bool{} altered := false for name, man := range c.Manifests { before, had := m.Manifests[name] if had && grantsOf(before) == grantsOf(man) { continue } altered = true for _, r := range append(slices.Clone(man.Requires), before.Requires...) { provisions[r] = true } } if !altered { return nil } var out []string for node, modules := range m.Assigned { if slices.Contains(composed, node) { continue } for _, mod := range modules { man := m.Manifests[mod] if man.ClaimsSeat("mesh-broker") || slices.ContainsFunc(man.Provides, func(o catalogue.Offer) bool { return provisions[o.Name] }) { out = append(out, node) break } } } sort.Strings(out) return out } 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 }