The provisioner, the assigner and the view read every node's composition (ADR 0593 §6); the runtime followed mesh-sdk's membership.StateIssued PerNode and process.NodeFamily on its own connection. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
112 lines
3.7 KiB
Go
112 lines
3.7 KiB
Go
//go:build integration
|
|
|
|
package bus_test
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// **A state its owner keeps one bucket per node is served to a bundle as one state** (novox/hq ADR 0593 §6: each node's
|
|
// engine writes its own `composed-<node>`, and the provisioner reads them all). Get goes to the bucket the key's first
|
|
// part names, Keys and Watch over every bucket of the prefix, a bucket created during the watch included, and a bucket
|
|
// that only starts like the prefix is never read.
|
|
func TestAPerNodeStateIsServedAcrossEveryNodesBucket(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
a, b := mesh.Bucket(t, "composed-a"), mesh.Bucket(t, "composed-b")
|
|
other := mesh.Bucket(t, "composedx-z")
|
|
for kv, key := range map[interface {
|
|
Put(string, []byte) (uint64, error)
|
|
}]string{a: "a.m1.f1", b: "b.m2.f2", other: "z.m3.f3"} {
|
|
if _, err := kv.Put(key, []byte(`{"v":1}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
prov := mt.MembershipOf("mesh-provisioner", "anchor", false, nil)
|
|
prov.State = []bus.StateIssued{{Name: "mesh-host.composed", Bucket: "composed-", PerNode: true}}
|
|
mesh.Issue(t, prov)
|
|
conn, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "anchor"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Logf = func(string, ...any) {}
|
|
t.Cleanup(conn.Close)
|
|
conn.Follow("mesh-provisioner")
|
|
mt.Until(t, func() error {
|
|
if conn.Membership("mesh-provisioner") == nil {
|
|
return errors.New("not issued yet")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "b.m2.f2"); err != nil || e == nil || e.Key != "b.m2.f2" {
|
|
t.Fatalf("get from the second node's bucket: %+v, %v", e, err)
|
|
}
|
|
if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "q.m.f"); err != nil || e != nil {
|
|
t.Fatalf("a key of a node with no bucket should read as none: %+v, %v", e, err)
|
|
}
|
|
if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "z.m3.f3"); err != nil || e != nil {
|
|
t.Fatalf("the look-alike bucket was read: %+v, %v", e, err)
|
|
}
|
|
if _, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "nonode"); err == nil {
|
|
t.Fatal("a key naming no node should be refused")
|
|
}
|
|
if keys, err := conn.StateKeys("mesh-provisioner", "mesh-host.composed"); err != nil || !reflect.DeepEqual(keys, []string{"a.m1.f1", "b.m2.f2"}) {
|
|
t.Fatalf("keys across the buckets: %v, %v", keys, err)
|
|
}
|
|
if _, err := conn.StatePut("mesh-provisioner", "mesh-host.composed", "a.m1.f9", []byte(`{}`)); err == nil {
|
|
t.Fatal("a per-node state is read only")
|
|
}
|
|
|
|
var mu sync.Mutex
|
|
var current, live []string
|
|
stop, err := conn.StateWatch("mesh-provisioner", "mesh-host.composed", "", func(ch bus.StateChange) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if ch.Current {
|
|
current = append(current, ch.Key)
|
|
} else {
|
|
live = append(live, ch.Key)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(stop)
|
|
mu.Lock()
|
|
sort.Strings(current)
|
|
if !reflect.DeepEqual(current, []string{"a.m1.f1", "b.m2.f2"}) {
|
|
t.Fatalf("current values: %v", current)
|
|
}
|
|
mu.Unlock()
|
|
|
|
// A third node's bucket, created during the watch, and a change in a first one.
|
|
c := mesh.Bucket(t, "composed-c")
|
|
if _, err := c.Put("c.m4.f4", []byte(`{"v":1}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := a.Put("a.m1.f5", []byte(`{"v":1}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mt.Until(t, func() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
got := strings.Join(append(append([]string(nil), current...), live...), ",")
|
|
for _, want := range []string{"a.m1.f5", "c.m4.f4"} {
|
|
if !strings.Contains(got, want) {
|
|
return fmt.Errorf("%s not heard yet: current %v, changes %v", want, current, live)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|