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
273 lines
13 KiB
Go
273 lines
13 KiB
Go
package frombustest
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
|
|
"git.novox.be/novox/mesh-sdk/go/compose/fromruntime"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// ADR 0570 clause 2 (issue 1954; a3's H5): the in-memory mesh composes a plan from the bus that hands restic to the
|
|
// provisioner and names its postgres-database, provided by postgres on n1. And one bad module must not drop its node
|
|
// (issue 774): with a module placed beside it whose built manifest cannot be used, or whose plan refuses, PlansHeld
|
|
// still hands restic's plan and names the bad one with why, where Plans refuses the whole node. A manifest the indexer
|
|
// holds that the mesh refuses (power's bar item with attributes its bar places by none of, mesh-catalog ca8d6f33,
|
|
// 2026-10-30) holds that module alone on its node, in the plans and in the node's composition (Compose), and on a node
|
|
// it is not placed on holds nothing.
|
|
func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
|
fp := "src2:" + strings.Repeat("9", 64)
|
|
for _, row := range []struct {
|
|
name string
|
|
bad string // the indexer's manifest of a module "backup" placed on n1; empty places none
|
|
// built is the manifest backup's done build names; empty gives it no build
|
|
built string
|
|
why string // words backup's hold says
|
|
// elsewhere leaves backup in the indexer's modules and places it on no node: nothing is held.
|
|
elsewhere bool
|
|
// refused is the indexer's verdict on bad, a problem its judge found (novox/hq ADR 0578 §1): Plans then holds
|
|
// backup too, and never ends the read.
|
|
refused string
|
|
// dataHeld says the data items' read holds backup too: a refusal it meets (a refused manifest, an unusable build,
|
|
// an item composition refuses), not one only a plan meets.
|
|
dataHeld bool
|
|
// dataOnly checks the data items' read alone: the other reads blame restic, whose templates collect the item.
|
|
dataOnly bool
|
|
}{
|
|
{name: "every module composes"},
|
|
{name: "a built manifest that cannot be used",
|
|
bad: `{"module":"backup","version":"1","build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`,
|
|
built: `{"module":"backup","version":"1","requires":"amqp"}`, why: "backup", dataHeld: true},
|
|
{name: "a plan that refuses",
|
|
bad: `{"module":"backup","version":"1","resources":[{"id":"conf","type":"file","path":"/etc/backup.conf",` +
|
|
`"mode":"0600","content":"key=${secret:nope}"}]}`, why: "${secret:nope}"},
|
|
{name: "an indexer's manifest the mesh refuses", bad: unusableItem, why: `"shows" is none of them`,
|
|
refused: `"shows" is none of them`, dataHeld: true},
|
|
// ADR 0578 §3: an accepted manifest whose data item its composition refuses is held by the culprit rule, in every read.
|
|
{name: "a data item its composition refuses",
|
|
bad: `{"module":"backup","version":"1","data":{"own":[{"id":"d","path":"${dir:nope}","class":"cache"}]}}`,
|
|
why: "nope", dataHeld: true, dataOnly: true},
|
|
{name: "an indexer's manifest the mesh refuses, placed elsewhere", bad: unusableItem, elsewhere: true,
|
|
refused: `"shows" is none of them`},
|
|
} {
|
|
b, err := Provisioned()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.bad != "" {
|
|
if !row.elsewhere {
|
|
p := b.Placement[Node]
|
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "backup"})
|
|
b.Placement[Node] = p
|
|
}
|
|
digest := delivery.ManifestDigest([]byte(row.bad))
|
|
b.Manifests[digest] = []byte(row.bad)
|
|
b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest}
|
|
if row.refused != "" {
|
|
b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest, Verdict: delivery.VerdictRefused,
|
|
Problems: []string{row.refused}}
|
|
}
|
|
if row.built != "" {
|
|
builtDigest := delivery.ManifestDigest([]byte(row.built))
|
|
b.Manifests[builtDigest] = []byte(row.built)
|
|
b.Builds["backup "+fp] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: builtDigest,
|
|
Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}}
|
|
}
|
|
if row.dataOnly {
|
|
items, _, iheld, err := frombus.DataItemsHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node})
|
|
if why, ok := iheld["backup"]; err != nil || !ok || !strings.Contains(why, row.why) || len(items["restic"]) == 0 {
|
|
t.Errorf("%s: data items %v held %v: %v", row.name, items, iheld, err)
|
|
}
|
|
continue
|
|
}
|
|
plans, err := frombus.Plans(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node})
|
|
switch {
|
|
case row.refused != "" && (err != nil || !planned(plans, "restic") || planned(plans, "backup")):
|
|
t.Errorf("%s: Plans did not hold the refused manifest alone: %v %v", row.name, plans, err)
|
|
case row.refused == "" && err == nil && !row.elsewhere:
|
|
t.Errorf("%s: Plans composed the node; the row does not refuse it", row.name)
|
|
}
|
|
}
|
|
if row.why != "" || row.bad == "" {
|
|
// The node's own composition holds by the same rule: every other module composed, the bad one named.
|
|
fps := FPs()
|
|
if row.bad != "" {
|
|
fps["backup"] = fp
|
|
}
|
|
out, _, held, err := frombus.Compose(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}, fps)
|
|
if err != nil {
|
|
t.Fatalf("%s: the node's composition: %v", row.name, err)
|
|
}
|
|
if key, _ := delivery.ComposedKey(Node, "restic", fps["restic"]); out[key].Module != "restic" {
|
|
t.Errorf("%s: restic is not composed (held %v)", row.name, held)
|
|
}
|
|
if why, ok := held["backup"]; (row.bad == "" && len(held) > 0) || (row.bad != "" && (!ok || !strings.Contains(why, row.why) || len(held) != 1)) {
|
|
t.Errorf("%s: the node's composition held %v, want backup alone, saying %q", row.name, held, row.why)
|
|
}
|
|
}
|
|
plans, held, err := frombus.PlansHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node})
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", row.name, err)
|
|
}
|
|
restic, handed := plans["restic"]
|
|
if !handed || len(restic.Provisions) != 1 || restic.Provisions[0].Provision != "postgres-database" ||
|
|
restic.Provisions[0].Provider != Node || restic.Provisions[0].ProviderModule != "postgres" {
|
|
t.Fatalf("%s: the plans are %+v (held %v)", row.name, plans, held)
|
|
}
|
|
// Issue 1954: the plans at the wanted builds and at the newest, over one read, are what PlansHeld answers for each,
|
|
// and where the time went is said by phase.
|
|
keptAt := FPs()
|
|
at, newest, took := frombus.PlansHeldTwice(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node, KeptAt: keptAt})
|
|
wantAt, heldAt, errAt := frombus.PlansHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node, KeptAt: keptAt})
|
|
if !reflect.DeepEqual(at, frombus.HeldPlans{Plans: wantAt, Held: heldAt, Err: errAt}) ||
|
|
!reflect.DeepEqual(newest, frombus.HeldPlans{Plans: plans, Held: held}) {
|
|
t.Errorf("%s: twice composed %+v and %+v, want %v %v %v and %v %v", row.name, at, newest, wantAt, heldAt, errAt, plans, held)
|
|
}
|
|
if said := frombus.PhasesSaid(took); !strings.HasPrefix(said, "read ") || !strings.Contains(said, "plans at newest ") {
|
|
t.Errorf("%s: the phases are said as %q", row.name, said)
|
|
}
|
|
if row.bad == "" || row.elsewhere {
|
|
if len(held) > 0 {
|
|
t.Errorf("%s: held %v", row.name, held)
|
|
}
|
|
continue
|
|
}
|
|
if why, ok := held["backup"]; !ok || !strings.Contains(why, row.why) || len(held) != 1 {
|
|
t.Errorf("%s: held %v, want backup alone, saying %q", row.name, held, row.why)
|
|
}
|
|
if _, planned := plans["backup"]; planned {
|
|
t.Errorf("%s: the held module has a plan", row.name)
|
|
}
|
|
// The data items are read by the same rule (ADR 0578 §3: disk-load's read): the bad module held, the read whole.
|
|
items, _, iheld, err := frombus.DataItemsHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node})
|
|
// A refusal only a plan meets (a file's secret) holds nothing here: data items render no such file.
|
|
mustHold := row.dataHeld
|
|
if why, ok := iheld["backup"]; err != nil || ok != mustHold || mustHold && !strings.Contains(why, row.why) || items["backup"] != nil {
|
|
t.Errorf("%s: data items %v held %v: %v", row.name, items, iheld, err)
|
|
}
|
|
}
|
|
|
|
// ADR 0580 clause 1: the watched reader reads a state it composes from out of the copy, a build by key each time it is
|
|
// asked, and a manifest once per digest.
|
|
byKey := map[string]int{}
|
|
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
|
|
byKey[state+" watched"]++
|
|
return handle(stdio.StateChange{Key: "placement." + Node, Op: stdio.OpPut, Value: []byte(`{}`), Current: true})
|
|
}, func(state, key string) ([]byte, bool, error) { byKey[state]++; return []byte(key), true, nil })
|
|
w := fromruntime.Watched{WatchedStates: ws}
|
|
for range 2 {
|
|
_, _, _ = w.Get(frombus.BuildsRead, "restic."+fp)
|
|
_, _, _ = w.Get(frombus.ManifestsRead, "sha256-d")
|
|
if _, found, _ := w.Get(frombus.PlacementRead, "placement."+Node); !found {
|
|
t.Error("the watched reader does not answer the placement from its copy")
|
|
}
|
|
}
|
|
if byKey[frombus.BuildsRead] != 2 || byKey[frombus.ManifestsRead] != 1 || byKey[frombus.PlacementRead] != 0 ||
|
|
byKey[frombus.PlacementRead+" watched"] != 1 {
|
|
t.Errorf("the watched reader read %v, want a build by key each time, a manifest once, the placement watched once", byKey)
|
|
}
|
|
fetched := 0
|
|
keep := fromruntime.KeptFetch(func(context.Context, string) ([]byte, error) { fetched++; return []byte("tar"), nil })
|
|
for range 2 {
|
|
if body, err := keep(context.Background(), "http://store/restic/files/blobs/sha256:a"); err != nil || string(body) != "tar" {
|
|
t.Fatalf("the kept fetch answered %q, %v", body, err)
|
|
}
|
|
}
|
|
if fetched != 1 {
|
|
t.Errorf("a templates archive was fetched %d times, want once", fetched)
|
|
}
|
|
|
|
// Issue 1954: the states reader takes a build in once per its content: composed again, its archive is not read again
|
|
// and the plans are the same.
|
|
b, err := Provisioned()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader, archives := frombus.StatesReader(statesOf(t, b), func(_ context.Context, url string) ([]byte, error) {
|
|
fetched++
|
|
_, digest, _ := strings.Cut(url, "/blobs/")
|
|
return b.Archives[digest], nil
|
|
}), fetched
|
|
var first map[string]catalogue.Plan
|
|
for i := range 2 {
|
|
plans, held, err := frombus.PlansHeld(context.Background(), reader, frombus.Mesh{}, frombus.Scope{Node: Node})
|
|
if err != nil || len(held) > 0 || (i == 1 && !reflect.DeepEqual(plans, first)) || !planned(plans, "restic") {
|
|
t.Fatalf("composed %v (held %v): %v", plans, held, err)
|
|
}
|
|
first = plans
|
|
}
|
|
if fetched-archives != 1 {
|
|
t.Errorf("restic's archive was read %d times over two compositions, want once", fetched-archives)
|
|
}
|
|
}
|
|
|
|
// statesOf is the bus b as a module's read states hold it.
|
|
func statesOf(t *testing.T, b *Bus) frombus.States {
|
|
t.Helper()
|
|
s := states{}
|
|
put := func(state, key string, raw []byte, err error) {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s[state] == nil {
|
|
s[state] = map[string][]byte{}
|
|
}
|
|
s[state][key] = raw
|
|
}
|
|
for node, p := range b.Placement {
|
|
raw, err := delivery.EncodePlacement(p)
|
|
put(frombus.PlacementRead, "placement."+node, raw, err)
|
|
}
|
|
for node, rec := range b.Record {
|
|
raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &rec})
|
|
put(frombus.HaveRead, node+".node", raw, err)
|
|
}
|
|
for module, m := range b.Module {
|
|
key, _ := delivery.ModulesKey(module)
|
|
raw, err := delivery.EncodeModule(m)
|
|
put(frombus.ModulesRead, key, raw, err)
|
|
}
|
|
for digest, man := range b.Manifests {
|
|
key, _ := delivery.ManifestsKey(digest)
|
|
raw, err := delivery.EncodeManifest(delivery.Manifest{Module: "m", JSON: string(man)})
|
|
put(frombus.ManifestsRead, key, raw, err)
|
|
}
|
|
for at, build := range b.Builds {
|
|
module, fp, _ := strings.Cut(at, " ")
|
|
key, _ := delivery.BuilderBuildKey(module, fp)
|
|
raw, err := delivery.EncodeBuilderBuild(*build)
|
|
put(frombus.BuildsRead, key, raw, err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// states are read states in memory.
|
|
type states map[string]map[string][]byte
|
|
|
|
func (s states) Get(state, key string) ([]byte, bool, error) {
|
|
v, ok := s[state][key]
|
|
return v, ok, nil
|
|
}
|
|
|
|
func (s states) Keys(state string) ([]string, error) {
|
|
var keys []string
|
|
for k := range s[state] {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
func planned[P any](plans map[string]P, module string) bool { _, ok := plans[module]; return ok }
|
|
|
|
// unusableItem is a manifest the mesh refuses as the node-engine did power's at mesh-catalog ca8d6f33: a bar block item
|
|
// with attributes its bar places by none of.
|
|
const unusableItem = `{"module":"backup","version":"1","moved":true,` +
|
|
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
|
|
`"items":[{"collection":"contribution:node-bar:block",` +
|
|
`"attributes":{"bar":"bottom","place":"status","shows":"state","state":"power.draw"},"template":"files/bar"}]}`
|