Files
mesh-sdk/go/pernode/pernode.go
T

346 lines
11 KiB
Go

// Package pernode reads a state its owner keeps one bucket per node as one state (novox/hq ADR 0593 §6: each node's
// engine writes its own `composed-<node>`, and the provisioner, the assigner and the view read them all), over any
// connection that satisfies Bus: a process's own (process.Conn) and the runtime's (node-tools) alike, so the rules are
// written once.
//
// A reader names the state as its manifest's `reads` does (`<module>.<state>`, such as `mesh-host.composed`) with the
// prefix of its buckets (`composed-`): each bucket `<prefix><node>` holds keys whose first part is that node, so a key
// names its bucket. Get reads one key from its node's bucket; Keys and Watch go over every bucket of the family the bus
// holds, and Watch takes in a node's bucket created after it began, as the bus says it (its creation advisory), so a
// node added later is heard with no timer. It reads only: the family has no write, and the owner's nodes write as their
// grants allow.
package pernode
import (
"context"
"errors"
"fmt"
"regexp"
"sort"
"strings"
"sync"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
// What a reader of a per-node state is granted beside each bucket's read (the grants part composes them): the listing
// of stream names (ListSubject) and the creation of any stream (CreatedAdvisory), which says a name only. A subject's
// wildcard is a whole token, so `KV_composed-*` cannot be granted: every creation is heard and the family's picked.
const (
ListSubject = "$JS.API.STREAM.NAMES"
CreatedAdvisory = "$JS.EVENT.ADVISORY.STREAM.CREATED.*"
// createdPrefix starts the subject of a key-value bucket's creation advisory, the bucket's name after it.
createdPrefix = "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_"
)
// Bus is what a connection gives the family: the names of the key-value buckets the bus holds, a bucket opened by name
// (jetstream.ErrBucketNotFound when it is not there), and the creation advisories (CreatedAdvisory) heard, each by its
// subject, until stop.
type Bus interface {
BucketNames(ctx context.Context) ([]string, error)
OpenBucket(ctx context.Context, name string) (jetstream.KeyValue, error)
SubscribeCreated(heard func(subject string)) (stop func(), err error)
}
// Family is a per-node state: its read name, its buckets' prefix, and the connection it is read over.
type Family struct {
Read string
Prefix string
Bus Bus
}
// Change is one change a watch hands over: the node whose bucket it is, the entry (its operation says put, delete or
// purge), and whether it was there when that bucket's watch began.
type Change struct {
Node string
Entry jetstream.KeyValueEntry
Current bool
}
// nodeName is a node a bucket and a subject token accept.
var nodeName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// Check refuses a family with no read name `<module>.<state>`, or a prefix a bucket name cannot start with or that
// does not end in `-` or `_`, since a prefix without its separator would read a look-alike family (`composed` reading
// `composedx-ace`).
func (f Family) Check() error {
module, name, ok := strings.Cut(f.Read, ".")
if !ok || module == "" || name == "" || strings.Contains(name, ".") {
return fmt.Errorf("%q is not a state another module keeps, <module>.<state>", f.Read)
}
if !nodeName.MatchString(strings.TrimRight(f.Prefix, "-_")) || !strings.HasSuffix(f.Prefix, "-") && !strings.HasSuffix(f.Prefix, "_") {
return fmt.Errorf("the state %s: %q is not a prefix of buckets, letters, digits, '-' and '_' ending in '-' or '_'", f.Read, f.Prefix)
}
if f.Bus == nil {
return fmt.Errorf("the state %s is read over no connection", f.Read)
}
return nil
}
// Bucket is the bucket of a node.
func (f Family) Bucket(node string) (string, error) {
if err := f.Check(); err != nil {
return "", err
}
if !nodeName.MatchString(node) {
return "", fmt.Errorf("the state %s: %q is not a node a bucket is named for", f.Read, node)
}
return f.Prefix + node, nil
}
// NodeOf is the node a key of the family names: its first part.
func NodeOf(key string) (string, error) {
node, _, ok := strings.Cut(key, ".")
if !ok || !nodeName.MatchString(node) {
return "", fmt.Errorf("%q names no node: a key of a per-node state starts <node>.", key)
}
return node, nil
}
// nodeOfBucket is the node of a bucket name of the family, and whether it is one.
func (f Family) nodeOfBucket(name string) (string, bool) {
node, ok := strings.CutPrefix(name, f.Prefix)
return node, ok && nodeName.MatchString(node)
}
// Nodes are the nodes whose bucket of the family the bus holds now, sorted.
func (f Family) Nodes(ctx context.Context) ([]string, error) {
if err := f.Check(); err != nil {
return nil, err
}
names, err := f.Bus.BucketNames(ctx)
if err != nil {
return nil, fmt.Errorf("the state %s: listing the buckets %s*: %w", f.Read, f.Prefix, err)
}
var nodes []string
for _, name := range names {
if node, ok := f.nodeOfBucket(name); ok {
nodes = append(nodes, node)
}
}
sort.Strings(nodes)
return nodes, nil
}
// Open is the bucket of a node, opened; nil, nil when the bus holds none.
func (f Family) Open(ctx context.Context, node string) (jetstream.KeyValue, error) {
bucket, err := f.Bucket(node)
if err != nil {
return nil, err
}
kv, err := f.Bus.OpenBucket(ctx, bucket)
if errors.Is(err, jetstream.ErrBucketNotFound) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("the state %s (bucket %s): %w", f.Read, bucket, err)
}
return kv, nil
}
// Get is a key's current entry, read from the bucket of the node the key names, or nil when it has none or that node
// has no bucket.
func (f Family) Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error) {
node, err := NodeOf(key)
if err != nil {
return nil, err
}
kv, err := f.Open(ctx, node)
if err != nil || kv == nil {
return nil, err
}
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("the state %s, key %s: %w", f.Read, key, err)
}
return e, nil
}
// Keys is every key with a current value in every bucket of the family, sorted.
func (f Family) Keys(ctx context.Context) ([]string, error) {
nodes, err := f.Nodes(ctx)
if err != nil {
return nil, err
}
keys := []string{}
for _, node := range nodes {
kv, err := f.Open(ctx, node)
if err != nil {
return nil, err
}
if kv == nil {
continue
}
k, err := kv.Keys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
continue
}
if err != nil {
return nil, fmt.Errorf("the state %s on %s, keys: %w", f.Read, node, err)
}
keys = append(keys, k...)
}
sort.Strings(keys)
return keys, nil
}
// Pattern is a key narrowing as the bus names it: `*` stays one name, `**` is the rest, empty is every key.
func Pattern(key string) string {
if key == "" {
return jetstream.AllKeys
}
tokens := strings.Split(key, ".")
for i, t := range tokens {
if t == "**" {
tokens[i] = ">"
}
}
return strings.Join(tokens, ".")
}
// Watch hands handle, one call at a time, the current values of every bucket of the family the bus holds (each
// Current, deletes left out), then every change, and the current values and changes of a bucket the bus creates later.
// It returns once the current values of the buckets there now are handled; an error before that is returned, and an
// error from handle after it ends that bucket's watch. key narrows it within each bucket (Pattern); a key whose first
// part names a node watches only that node's bucket. ctx ends every watch and the hearing of new buckets.
func (f Family) Watch(ctx context.Context, key string, handle func(Change) error) error {
if err := f.Check(); err != nil {
return err
}
var mu sync.Mutex
one := func(c Change) error {
mu.Lock()
defer mu.Unlock()
return handle(c)
}
var wmu sync.Mutex
watched := map[string]bool{}
watch := func(node string) error {
wmu.Lock()
if watched[node] {
wmu.Unlock()
return nil
}
watched[node] = true
wmu.Unlock()
kv, err := f.Open(ctx, node)
if err != nil || kv == nil {
return err
}
return f.watchBucket(ctx, kv, node, key, one)
}
if first, _, _ := strings.Cut(key, "."); key != "" && first != "*" && first != "**" {
return watch(first)
}
// Heard before the buckets are listed, so a bucket created between the two is not missed.
stop, err := f.Bus.SubscribeCreated(func(subject string) {
bucket, isKV := strings.CutPrefix(subject, createdPrefix)
if node, ok := f.nodeOfBucket(bucket); isKV && ok {
go func() { _ = watch(node) }()
}
})
if err != nil {
return fmt.Errorf("the state %s: hearing new buckets %s*: %w", f.Read, f.Prefix, err)
}
go func() { <-ctx.Done(); stop() }()
nodes, err := f.Nodes(ctx)
if err != nil {
stop()
return err
}
for _, node := range nodes {
if err := watch(node); err != nil {
stop()
return err
}
}
return nil
}
// watchBucket hands one bucket's current values, then its changes from a goroutine, until ctx ends or handle errs.
func (f Family) watchBucket(ctx context.Context, kv jetstream.KeyValue, node, key string, handle func(Change) error) error {
w, err := kv.Watch(ctx, Pattern(key))
if err != nil {
return fmt.Errorf("the state %s on %s, watching %q: %w", f.Read, node, key, err)
}
for {
select {
case <-ctx.Done():
_ = w.Stop()
return ctx.Err()
case e, open := <-w.Updates():
if !open {
return fmt.Errorf("the state %s on %s: the watch ended", f.Read, node)
}
if e == nil {
go func() {
defer func() { _ = w.Stop() }()
for {
select {
case <-ctx.Done():
return
case e, open := <-w.Updates():
if !open || e == nil {
return
}
if handle(Change{Node: node, Entry: e}) != nil {
return
}
}
}
}()
return nil
}
if e.Operation() != jetstream.KeyValuePut {
continue
}
if err := handle(Change{Node: node, Entry: e, Current: true}); err != nil {
_ = w.Stop()
return err
}
}
}
}
// OverNATS is the Bus of a plain connection: its JetStream's bucket names and buckets, and the creation advisories
// subscribed on it. A connection that words a refused open its own way (process.Conn) gives its own OpenBucket.
func OverNATS(nc *nats.Conn) Bus { return natsBus{nc: nc} }
type natsBus struct{ nc *nats.Conn }
func (b natsBus) BucketNames(ctx context.Context) ([]string, error) {
js, err := jetstream.New(b.nc)
if err != nil {
return nil, err
}
lister := js.KeyValueStoreNames(ctx)
var names []string
for name := range lister.Name() {
names = append(names, name)
}
if err := lister.Error(); err != nil && !errors.Is(err, jetstream.ErrEndOfData) {
return nil, err
}
return names, nil
}
func (b natsBus) OpenBucket(ctx context.Context, name string) (jetstream.KeyValue, error) {
js, err := jetstream.New(b.nc)
if err != nil {
return nil, err
}
return js.KeyValue(ctx, name)
}
func (b natsBus) SubscribeCreated(heard func(subject string)) (func(), error) {
sub, err := b.nc.Subscribe(CreatedAdvisory, func(m *nats.Msg) { heard(m.Subject) })
if err != nil {
return nil, err
}
return func() { _ = sub.Unsubscribe() }, nil
}