mesh/delivery merged: source.main moved to novox/mesh-sdk@cad8250aaa8c
mesh/delivery-group group feat/l5-l7-main merged: every member is merged
mesh/check pass 790d1c82d027 on da41823855a9 | unit 1964 run, 5697 kept, 30 skipped | integration 1999 run, 5829 kept, 38 skipped | over bound: unit, …
A read could start a state's watch before its follower joined, and a change landing in that gap woke nothing (review of ADR 0580 §2): a joining follower now runs over the copy's current values before any later change. The watched reader keeps each archive once per URL, and both caches are bounded. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
180 lines
9.2 KiB
Go
180 lines
9.2 KiB
Go
package frombustest
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"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)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
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"}]}`
|