The mirror composed every node in full on the record mirrors' 10 s tick; a pass takes about 45 s on the control node, so the controller never rested (130-175 % CPU, issue 1945 note 22). It now passes on a watch of the compositions' digests and of the assignments, one pass at a time with the wakes of a pass coalesced into one after it (at least 30 s apart), skips the composition when the stores' Input digest is unchanged, and keeps a safety pass every 10 minutes that composes nothing when nothing changed.
116 lines
4.1 KiB
Go
116 lines
4.1 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// 1840/AC5 (ADR 0507 §5): the source of the declared-packages mirror composes each node and names, for a package two modules
|
|
// declare and one declared absent, the modules that declare it; the same records give the same bytes on a second pass, so
|
|
// the mirror writes nothing; a node with no package has a key that says so.
|
|
func TestTheDeclaredPackagesMirrorNamesWhatEachNodesModulesDeclare(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "viewer", Version: "1", Resources: []map[string]any{
|
|
{"id": "pkg-curl", "type": "package", "package": "curl"},
|
|
{"id": "pkg-nano", "type": "package", "package": "nano", "absent": true}}})
|
|
register(t, open, catalogue.Manifest{Module: "fetcher", Version: "1", Resources: []map[string]any{
|
|
{"id": "pkg-curl", "type": "package", "package": "curl"}}})
|
|
for _, module := range []string{"viewer", "fetcher"} {
|
|
if _, err := assign(ctx, open, "anchor", module); err != nil {
|
|
t.Fatalf("assign anchor %s: %v", module, err)
|
|
}
|
|
}
|
|
source := declaredPackagesMirror(open)
|
|
first, err := source(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got inventory.DeclaredPackagesMirrored
|
|
if err := json.Unmarshal(first["anchor"], &got); err != nil {
|
|
t.Fatalf("anchor has no value: %v", err)
|
|
}
|
|
// Every node also runs the mesh's own modules, which declare packages of their own: curl and nano are looked
|
|
// up by name, and no package names viewer or fetcher but those they declare.
|
|
byName := map[string]inventory.PackageDeclared{}
|
|
for _, p := range got.Packages {
|
|
byName[p.Package] = p
|
|
}
|
|
same := func(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
for _, p := range []inventory.PackageDeclared{{Package: "curl", Modules: []string{"fetcher", "viewer"}},
|
|
{Package: "nano", Modules: []string{"viewer"}, Absent: true}} {
|
|
if g, ok := byName[p.Package]; !ok || g.Absent != p.Absent || !same(g.Modules, p.Modules) {
|
|
t.Errorf("anchor declares %s as %+v, want %+v (all: %+v)", p.Package, g, p, got.Packages)
|
|
}
|
|
}
|
|
for _, p := range got.Packages {
|
|
if p.Package == "curl" || p.Package == "nano" {
|
|
continue
|
|
}
|
|
for _, m := range p.Modules {
|
|
if m == "viewer" || m == "fetcher" {
|
|
t.Errorf("%s is said to be declared by %s, which does not declare it", p.Package, m)
|
|
}
|
|
}
|
|
}
|
|
var laptop inventory.DeclaredPackagesMirrored
|
|
if err := json.Unmarshal(first["laptop"], &laptop); err != nil || laptop.Node != "laptop" || laptop.Packages == nil {
|
|
t.Errorf("a node assigned neither module has no value of its own: %s (%v)", first["laptop"], err)
|
|
}
|
|
for _, p := range laptop.Packages {
|
|
if p.Package == "curl" || p.Package == "nano" {
|
|
t.Errorf("laptop is assigned neither module and declares %+v", p)
|
|
}
|
|
}
|
|
second, err := source(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for node, value := range first {
|
|
if string(second[node]) != string(value) {
|
|
t.Errorf("the value of %s changed between two passes over the same records:\n%s\n%s", node, value, second[node])
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1945/AC-mirror-5: the digest the declared-packages mirror skips an unchanged pass by is made from a real reading of the
|
|
// stores, is the same over two readings of the same records, and moves when an assignment does.
|
|
func TestTheInputDigestOfTheStoresIsStable(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "viewer", Version: "1", Resources: []map[string]any{
|
|
{"id": "pkg-curl", "type": "package", "package": "curl"}}})
|
|
read := func() string {
|
|
in, err := readComposeInput(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return inputDigest(in)
|
|
}
|
|
first, second := read(), read()
|
|
if first == "" || first != second {
|
|
t.Fatalf("two readings of the same records gave the digests %q and %q", first, second)
|
|
}
|
|
if _, err := assign(ctx, open, "anchor", "viewer"); err != nil {
|
|
t.Fatalf("assign: %v", err)
|
|
}
|
|
if read() == first {
|
|
t.Errorf("an assignment left the digest of the stores' Input at %s", first)
|
|
}
|
|
}
|