Compose kept compositions again when a module's config changes (issue 1945, ADR 0545)
The controller composed a module only at the fingerprint it is registered at, so each of the up to five compositions kept for going back carried the config of the moment it was made: a want back to an earlier mesh-build-go on g14 put its capacity back to 0. Nothing composed a node when mesh-config's resolved values changed, either. Now a write of mesh-config_resolved (<node>.<module>.<key>), heard from that bucket's watch and gathered for a second, drops the controller's 5 s reading of the values, composes the node, and composes every kept fingerprint of the touched modules on that node again from its own build's manifest with today's inputs, writing only the compositions whose digest differs. A starting controller does it once for every node, for a change no controller heard. The two replaced-controller tests are one table test with the kept cases. Research 072: milestone 2, the composer reacting to the change of an input.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -46,51 +47,91 @@ func TestTheFirstStartDeletesThePlansBucketsAndSaysEachOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1945/AC6 (unit half): the controller this one replaced is composed on its node from its own build's manifest at its
|
||||
// own fingerprint: the composition holds what that manifest declares, not what the controller registered now declares.
|
||||
func TestTheReplacedControllerIsComposedFromItsOwnManifest(t *testing.T) {
|
||||
now := catalogue.Manifest{Module: controllerModule, Version: "6", Resources: []map[string]any{
|
||||
{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings", "content": "six\n"}}}
|
||||
before := catalogue.Manifest{Module: controllerModule, Version: "5", Resources: []map[string]any{
|
||||
{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings", "content": "five\n"}}}
|
||||
in := compose.Input{Catalogue: map[string]catalogue.Manifest{controllerModule: now},
|
||||
Nodes: []compose.Node{{Name: "anchor", Assigned: []string{controllerModule}, Stated: true}}}
|
||||
fp := sdkdelivery.SourcePrefix2 + strings.Repeat("5", 64)
|
||||
c, err := replacedComposition(in, replacedController{node: "anchor", fp: fp, manifest: before})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Module != controllerModule || c.FP != fp || c.Node != "anchor" || len(c.Resources) != 1 {
|
||||
t.Fatalf("composed %s at %s on %s with %d resources", c.Module, c.FP, c.Node, len(c.Resources))
|
||||
}
|
||||
var f struct{ Content string }
|
||||
_ = json.Unmarshal(c.Resources[0], &f)
|
||||
if f.Content != "five\n" {
|
||||
t.Errorf("the replaced controller's composition holds %q, its own manifest says five", f.Content)
|
||||
}
|
||||
if in.Catalogue[controllerModule].Version != "6" {
|
||||
t.Errorf("composing the replaced controller changed the Input's catalogue")
|
||||
}
|
||||
}
|
||||
// An earlier fingerprint is composed from its own build's manifest with today's inputs: the controller this one
|
||||
// replaced (1945/AC6), and every kept composition of a module whose config changed (ADR 0545: on 2026-10-25 a want back
|
||||
// to an earlier mesh-build-go on g14 applied the capacity of 0 its kept composition was made with). The composition
|
||||
// holds what that manifest declares and today's config, the kept keys are those of the touched modules at a
|
||||
// fingerprint other than the registered one, the build of a fingerprint is its newest that worked, and a write of
|
||||
// mesh-config's resolved values names the node and module it touches.
|
||||
func TestAnEarlierFingerprintIsComposedFromItsOwnManifest(t *testing.T) {
|
||||
fp := func(c string) string { return sdkdelivery.SourcePrefix2 + strings.Repeat(c, 64) }
|
||||
t.Run("from its own manifest, with today's inputs", func(t *testing.T) {
|
||||
manifest := func(version string) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: controllerModule, Version: version,
|
||||
Settings: map[string]catalogue.SettingDeclaration{
|
||||
"capacity": {Kind: catalogue.KindPreference, Default: float64(0), Why: "how many builds at once"}},
|
||||
Resources: []map[string]any{{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings",
|
||||
"content": version + " ${setting:capacity}\n"}}}
|
||||
}
|
||||
now, before := manifest("six"), manifest("five")
|
||||
in := compose.Input{Catalogue: map[string]catalogue.Manifest{controllerModule: now},
|
||||
Nodes: []compose.Node{{Name: "anchor", Assigned: []string{controllerModule}, Stated: true,
|
||||
Settings: map[string][]catalogue.Layer{controllerModule: {{From: "mesh", Values: map[string]any{"capacity": float64(4)}}}}}}}
|
||||
for name, compose := range map[string]func() (sdkdelivery.Composed, error){
|
||||
"the replaced controller": func() (sdkdelivery.Composed, error) {
|
||||
return replacedComposition(in, replacedController{node: "anchor", fp: fp("5"), manifest: before})
|
||||
},
|
||||
"a kept composition": func() (sdkdelivery.Composed, error) {
|
||||
return keptComposition(in, "anchor", controllerModule, fp("5"), before)
|
||||
},
|
||||
} {
|
||||
c, err := compose()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
if c.Module != controllerModule || c.FP != fp("5") || c.Node != "anchor" || len(c.Resources) != 1 {
|
||||
t.Fatalf("%s: composed %s at %s on %s with %d resources", name, c.Module, c.FP, c.Node, len(c.Resources))
|
||||
}
|
||||
var f struct{ Content string }
|
||||
_ = json.Unmarshal(c.Resources[0], &f)
|
||||
if f.Content != "five 4\n" {
|
||||
t.Errorf("%s holds %q, want its own manifest's five with today's capacity 4", name, f.Content)
|
||||
}
|
||||
}
|
||||
if in.Catalogue[controllerModule].Version != "six" {
|
||||
t.Errorf("composing an earlier fingerprint changed the Input's catalogue")
|
||||
}
|
||||
})
|
||||
|
||||
// 1945/AC6 (unit half): the controller replaced is the newest earlier build of the controller that worked and names a
|
||||
// fingerprint of the new form, not the one registered now.
|
||||
func TestTheReplacedControllerIsItsNewestEarlierBuild(t *testing.T) {
|
||||
now, before, older := sdkdelivery.SourcePrefix2+strings.Repeat("6", 64), sdkdelivery.SourcePrefix2+strings.Repeat("5", 64),
|
||||
sdkdelivery.SourcePrefix2+strings.Repeat("4", 64)
|
||||
m := []byte(`{"module":"mesh-controller","version":"5"}`)
|
||||
builds := []inventory.Build{
|
||||
{ID: "c6", SourceFingerprint: now, Manifest: m},
|
||||
{ID: "c5-failed", SourceFingerprint: before, Failed: "no", Manifest: m},
|
||||
{ID: "c5", SourceFingerprint: before, Manifest: m},
|
||||
{ID: "c4", SourceFingerprint: older, Manifest: m},
|
||||
}
|
||||
if b, ok := replacedBuild(builds, now); !ok || b.ID != "c5" {
|
||||
t.Errorf("the replaced build is %+v (%v), want c5", b, ok)
|
||||
}
|
||||
if _, ok := replacedBuild(builds[:1], now); ok {
|
||||
t.Errorf("a controller with no earlier build replaced one")
|
||||
}
|
||||
t.Run("which build, which keys, which touch", func(t *testing.T) {
|
||||
m := []byte(`{"module":"mesh-controller","version":"5"}`)
|
||||
builds := []inventory.Build{
|
||||
{ID: "c6", SourceFingerprint: fp("6"), Manifest: m},
|
||||
{ID: "c5-failed", SourceFingerprint: fp("5"), Failed: "no", Manifest: m},
|
||||
{ID: "c5", SourceFingerprint: fp("5"), Manifest: m},
|
||||
{ID: "c4", SourceFingerprint: fp("4"), Manifest: m},
|
||||
}
|
||||
if b, ok := replacedBuild(builds, fp("6")); !ok || b.ID != "c5" {
|
||||
t.Errorf("the replaced build is %+v (%v), want c5", b, ok)
|
||||
}
|
||||
if _, ok := replacedBuild(builds[:1], fp("6")); ok {
|
||||
t.Errorf("a controller with no earlier build replaced one")
|
||||
}
|
||||
if b, ok := buildAt(builds, fp("5")); !ok || b.ID != "c5" {
|
||||
t.Errorf("the build at 5 is %+v (%v), want c5, the one that worked", b, ok)
|
||||
}
|
||||
if _, ok := buildAt(builds, fp("3")); ok {
|
||||
t.Errorf("a fingerprint with no build has one")
|
||||
}
|
||||
key := func(node, module, f string) string { k, _ := sdkdelivery.ComposedKey(node, module, f); return k }
|
||||
keys := []string{key("g14", "mesh-build-go", fp("2")), key("g14", "mesh-build-go", fp("1")),
|
||||
key("g14", "mesh-build-go", fp("3")), key("g14", "hello-web", fp("1")), key("ace", "mesh-build-go", fp("1"))}
|
||||
registered := map[string]string{"mesh-build-go": fp("3"), "hello-web": fp("9")}
|
||||
got := keptToFollow(keys, "g14", map[string]bool{"mesh-build-go": true}, registered)
|
||||
if want := []keptKey{{"mesh-build-go", fp("1")}, {"mesh-build-go", fp("2")}}; !slices.Equal(got, want) {
|
||||
t.Errorf("kept to follow %v, want %v", got, want)
|
||||
}
|
||||
if got := keptToFollow(keys, "g14", nil, registered); len(got) != 3 {
|
||||
t.Errorf("every module's kept compositions: %v", got)
|
||||
}
|
||||
for k, want := range map[string][2]string{"g14.mesh-build-go.capacity": {"g14", "mesh-build-go"},
|
||||
"g14.mesh-build-go.limits.cpu": {"g14", "mesh-build-go"}, "g14.mesh-build-go": {}, "": {}} {
|
||||
node, module, ok := configTouched(k)
|
||||
if [2]string{node, module} != want || ok != (want[0] != "") {
|
||||
t.Errorf("%q touches %s %s (%v), want %v", k, node, module, ok, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 1945/AC14 (unit half): the declarations' stream stays while one node's have names another node-engine than the one
|
||||
|
||||
Reference in New Issue
Block a user