Serve a per-node state to a bundle as one state: get by the key's node, keys and watch over every node's bucket, a later bucket too

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
This commit is contained in:
2026-11-02 18:27:34 +01:00
parent d7d908cf67
commit 8114b45884
3 changed files with 334 additions and 1 deletions
+38 -1
View File
@@ -28,6 +28,10 @@ type StateIssued struct {
Name string `json:"name"`
Bucket string `json:"bucket"`
Writes bool `json:"writes,omitempty"`
// PerNode says the state is kept one bucket per node (novox/hq ADR 0593 §6, mesh-sdk membership.StateIssued): Bucket is
// then the buckets' prefix, each bucket the prefix and a node's name, and a key's first part names its node. It is read
// only, served as one state (per_node.go).
PerNode bool `json:"per-node,omitempty"`
}
// StateEntry is one key's current value.
@@ -81,6 +85,9 @@ func (c *Conn) issuedState(module, name string) (StateIssued, error) {
module, name, issued)
}
// errNoBucket marks a bucket the bus does not hold yet.
var errNoBucket = errors.New("no such bucket")
func (c *Conn) bucket(ctx context.Context, s StateIssued) (jetstream.KeyValue, error) {
js, err := jetstream.New(c.nc)
if err != nil {
@@ -90,7 +97,7 @@ func (c *Conn) bucket(ctx context.Context, s StateIssued) (jetstream.KeyValue, e
if err != nil {
if errors.Is(err, jetstream.ErrBucketNotFound) {
return nil, fmt.Errorf("the state %q is issued and its bucket is not on the bus yet: the mesh "+
"creates it from the catalogue on its next raise", s.Name)
"creates it from the catalogue on its next raise: %w", s.Name, errNoBucket)
}
return nil, err
}
@@ -114,10 +121,18 @@ func (c *Conn) StateGet(module, name, key string) (*StateEntry, error) {
if err := checkKey(key); err != nil {
return nil, err
}
if s.PerNode {
if s, err = nodeBucketOf(s, key); err != nil {
return nil, err
}
}
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
defer cancel()
kv, err := c.bucket(ctx, s)
if err != nil {
if s.PerNode && errors.Is(err, errNoBucket) {
return nil, nil
}
return nil, err
}
e, err := kv.Get(ctx, key)
@@ -149,6 +164,9 @@ func (c *Conn) StateHistory(module, name, key string) ([]StateKept, error) {
if err := checkKey(key); err != nil {
return nil, err
}
if s.PerNode {
return nil, perNodeRefusal(module, name, "history")
}
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
defer cancel()
kv, err := c.bucket(ctx, s)
@@ -199,6 +217,9 @@ func (c *Conn) StateInfo(module, name string) (*StateInfo, error) {
if err != nil {
return nil, err
}
if s.PerNode {
return nil, perNodeRefusal(module, name, "fill")
}
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
defer cancel()
kv, err := c.bucket(ctx, s)
@@ -325,12 +346,20 @@ func (c *Conn) StateKeys(module, name string) ([]string, error) {
if err != nil {
return nil, err
}
if s.PerNode {
return c.perNodeKeys(s)
}
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
defer cancel()
kv, err := c.bucket(ctx, s)
if err != nil {
return nil, err
}
return keysOf(ctx, kv)
}
// keysOf is every key with a current value in one bucket, sorted.
func keysOf(ctx context.Context, kv jetstream.KeyValue) ([]string, error) {
lister, err := kv.ListKeys(ctx)
if err != nil {
return nil, err
@@ -392,12 +421,20 @@ func (c *Conn) StateWatch(module, name, pattern string, deliver func(StateChange
}
filter = strings.Join(parts, ".")
}
if s.PerNode {
return c.perNodeWatch(module, s, filter, deliver)
}
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
kv, err := c.bucket(ctx, s)
cancel()
if err != nil {
return nil, err
}
return c.watchBucket(module, name, kv, filter, deliver)
}
// watchBucket is StateWatch over one bucket: the current values, then every change.
func (c *Conn) watchBucket(module, name string, kv jetstream.KeyValue, filter string, deliver func(StateChange) error) (stop func(), err error) {
watching, stopWatching := context.WithCancel(context.Background())
w, err := kv.Watch(watching, filter)
if err != nil {