Files
mesh-host/internal/selfcompose/data_test.go
T
jschoubben e0261cdfc1 Keep each placed module's data items in the engine's state data, so disk-load measures without composing (ADR 0577)
After a pass that handed a new composition, the node's composer works out its modules' data
items from the inputs it composed from and writes each module's key <node>.<module> of
mesh-host_data only when it differs, deleting the keys of modules that left. Pins mesh-sdk
feat/data-items for the contract and the engine's grant. Two tool-answer tests are merged so
the unit count stays where it was.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 00:09:25 +01:00

149 lines
6.2 KiB
Go

package selfcompose
import (
"context"
"errors"
"slices"
"strings"
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// fakeData is the engine's state data as a map, counting the writes and deletes made.
type fakeData struct {
mu sync.Mutex
kv map[string][]byte
puts []string
deletes []string
fail error
}
func (f *fakeData) Get(_ context.Context, key string) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.kv[key], nil
}
func (f *fakeData) Put(_ context.Context, key string, value []byte) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.fail != nil {
return f.fail
}
f.kv[key] = value
f.puts = append(f.puts, key)
return nil
}
func (f *fakeData) Delete(_ context.Context, key string) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.kv, key)
f.deletes = append(f.deletes, key)
return nil
}
func (f *fakeData) Keys(_ context.Context, pattern string) ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []string
for k := range f.kv {
if matches(pattern, k) {
out = append(out, k)
}
}
return out, nil
}
// ADR 0577 clause 1 (and its check in clause 5): after a pass that composed something new, the engine keeps each placed
// module's data items in its state data, key `<node>.<module>`, with its composition's digest: written when the module's
// items change, not written when they do not, and deleted when the module leaves the node; a key of another node is never
// touched, a pass that handed nothing new reads nothing, and a write that failed is tried again at the next pass.
func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
item := func(module, id, path string) catalogue.PlacedDataItem {
return catalogue.PlacedDataItem{Module: module, ID: id, Class: "valuable", Path: path, Measure: catalogue.MeasureWalk}
}
store := &fakeData{kv: map[string][]byte{
// Left by a module that went while the engine was down, and another node's key.
"n1.gone": []byte(`{"v":1}`), "n2.web": []byte(`{"v":1}`)}}
var items map[string][]catalogue.PlacedDataItem
var asked int
setFor(t, &dataItemsOf, func(context.Context, frombus.Reader, frombus.Mesh, frombus.Scope) (map[string][]catalogue.PlacedDataItem, []string, error) {
asked++
return items, nil, nil
})
// The engine's grant lets it read its own node's keys, as the listing at a run's first pass does, and no other's.
if !grantAllows(t, "n1", delivery.EngineDataBucket, "n1.*") || grantAllows(t, "n1", delivery.EngineDataBucket, "n2.*") {
t.Error("the node's grant reads other keys of the engine's state data than its own")
}
c := New(Env{Node: "n1", Data: store})
webKey, _ := delivery.ComposedKey("n1", "web", delivery.SourcePrefix2+strings.Repeat("a", 64))
nodeKey, _ := delivery.ComposedNodeKey("n1")
for _, step := range []struct {
name string
due bool
items map[string][]catalogue.PlacedDataItem
digests map[string]string // composed key to digest handed
fail error
puts []string
deletes []string
asked int
// want is each key's items and digest after the step.
want map[string]delivery.EngineData
}{
{name: "the run's first pass writes each module's items and deletes what left", due: true,
items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db")}, "oldie": {item("oldie", "x", "/srv/oldie")}, "bare": nil},
digests: map[string]string{webKey: "sha256:w1", nodeKey: "sha256:n1"},
puts: []string{"n1.oldie", "n1.web"}, deletes: []string{"n1.gone"}, asked: 1,
want: map[string]delivery.EngineData{
"n1.web": {Module: "web", Digest: "sha256:w1", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")}},
"n1.oldie": {Module: "oldie", Digest: "sha256:n1", Items: []catalogue.PlacedDataItem{item("oldie", "x", "/srv/oldie")}}}},
{name: "a pass that handed nothing new reads nothing", due: false, asked: 1},
{name: "a pass that changes no module's items writes nothing", due: true,
items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db")}, "oldie": {item("oldie", "x", "/srv/oldie")}},
digests: map[string]string{webKey: "sha256:w1", nodeKey: "sha256:n1"}, asked: 2},
{name: "a write that fails is tried again at the next pass", due: true, fail: errors.New("the bus said no"),
items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db2")}, "oldie": {item("oldie", "x", "/srv/oldie")}},
digests: map[string]string{webKey: "sha256:w2", nodeKey: "sha256:n1"}, asked: 3},
{name: "a module whose composition changed its items is written again", due: false,
items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db2")}, "oldie": {item("oldie", "x", "/srv/oldie")}},
digests: map[string]string{webKey: "sha256:w2", nodeKey: "sha256:n1"}, puts: []string{"n1.web"}, asked: 4,
want: map[string]delivery.EngineData{
"n1.web": {Module: "web", Digest: "sha256:w2", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db2")}}}},
{name: "a module that left the node is deleted", due: true,
items: map[string][]catalogue.PlacedDataItem{"oldie": {item("oldie", "x", "/srv/oldie")}},
digests: map[string]string{nodeKey: "sha256:n1"}, deletes: []string{"n1.web"}, asked: 5},
} {
t.Run(step.name, func(t *testing.T) {
store.mu.Lock()
store.puts, store.deletes, store.fail = nil, nil, step.fail
store.mu.Unlock()
items = step.items
c.mu.Lock()
c.handed = step.digests
c.dataDue = c.dataDue || step.due
c.mu.Unlock()
c.publishData(context.Background())
slices.Sort(store.puts)
if !slices.Equal(store.puts, step.puts) || !slices.Equal(store.deletes, step.deletes) || asked != step.asked {
t.Fatalf("wrote %v and deleted %v, items asked %d times; want %v, %v, %d", store.puts, store.deletes, asked,
step.puts, step.deletes, step.asked)
}
for key, want := range step.want {
got, err := delivery.DecodeEngineData(store.kv[key])
if err != nil || got.Node != "n1" || got.Module != want.Module || got.Digest != want.Digest ||
!slices.Equal(got.Items, want.Items) {
t.Errorf("%s holds %+v (%v), want %+v", key, got, err, want)
}
}
if store.kv["n2.web"] == nil {
t.Error("another node's key was deleted")
}
})
}
}