Files
mesh-controller/cmd/mesh-controller/states_announced.go
T

226 lines
7.6 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"os"
"strings"
"git.novox.be/novox/mesh-sdk/go/announce"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory"
)
// What each module keeps, and who may read it (novox/hq ADR 0495 clauses 2 to 5, ADR 0513 §3; issue 1790, D1-D3).
//
// Every module says on the bus which states it keeps and which of them others may read (announce.Subject), from its
// code; the controller says its own (D1). The serving controller keeps the last word of each module in its bucket
// (D2), and the bus's users are composed from it: a reader's `reads: <owner>.<name>` is granted only when the owner
// announced that state readable; an owner that has announced nothing yet is read from `reads` as before (the
// transition), and `status` names it. A changed word composes the bus's users again: the machine holding the bus is
// sent its new user list (D3). No timer: the words wake it.
// announceOwnStates says what the controller keeps, on its own event subject (D1).
func announceOwnStates(ctx context.Context, js *broker.JetStream) error {
host, _ := os.Hostname()
body, err := announce.Encode(broker.ControllerAnnouncement(host))
if err != nil {
return err
}
api, err := jetstream.New(js.Conn())
if err != nil {
return err
}
if _, err := api.Publish(ctx, announce.Subject(broker.ControllerSeat), body); err != nil {
return fmt.Errorf("saying what the controller keeps: %w", err)
}
return nil
}
// keepAnnouncements follows every module's word on what it keeps (D2) and keeps the last of each, from the last each
// module said: one module's word in the bucket under its name. A word whose module is not the subject's is refused,
// said; a changed word composes the bus's users again (D3).
func keepAnnouncements(ctx context.Context, js *broker.JetStream, open *stores) error {
api, err := jetstream.New(js.Conn())
if err != nil {
return err
}
kv, err := api.KeyValue(ctx, broker.StatesAnnouncedBucket)
if err != nil {
return fmt.Errorf("the bucket of the states announced: %w", err)
}
stream, err := api.Stream(ctx, broker.EventsStream)
if err != nil {
return fmt.Errorf("the events stream: %w", err)
}
consumer, err := stream.OrderedConsumer(ctx, jetstream.OrderedConsumerConfig{
FilterSubjects: []string{announce.SubjectAll}, DeliverPolicy: jetstream.DeliverLastPerSubjectPolicy})
if err != nil {
return fmt.Errorf("following the states announced: %w", err)
}
consuming, err := consumer.Consume(func(m jetstream.Msg) {
changed, err := keepAnnouncement(ctx, kv, m.Subject(), m.Data())
if err != nil {
fmt.Printf("states: %v\n", err)
return
}
if changed {
inBackground(func() { composeAnnouncedReads(context.WithoutCancel(ctx), open) })
}
})
if err != nil {
return fmt.Errorf("following the states announced: %w", err)
}
go func() {
<-ctx.Done()
consuming.Stop()
}()
return nil
}
// announcedKeeper is the bucket of the states announced, as keepAnnouncement writes it.
type announcedKeeper interface {
Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error)
Put(ctx context.Context, key string, value []byte) (uint64, error)
}
// keepAnnouncement keeps one module's word, and says whether it changed what the bucket held.
func keepAnnouncement(ctx context.Context, kv announcedKeeper, subject string, body []byte) (bool, error) {
s, err := announce.Decode(body)
if err != nil {
return false, fmt.Errorf("a word on %s is not read: %w", subject, err)
}
if subject != announce.Subject(s.Module) {
return false, fmt.Errorf("%s says what %s keeps, and a module says only what it keeps itself: refused", subject,
s.Module)
}
key, err := announce.Key(s.Module)
if err != nil {
return false, err
}
stored, err := announce.Encode(s)
if err != nil {
return false, err
}
if was, err := kv.Get(ctx, key); err == nil {
if string(was.Value()) == string(stored) {
return false, nil
}
} else if !errors.Is(err, jetstream.ErrKeyNotFound) {
return false, fmt.Errorf("the last word of %s cannot be read: %w", s.Module, err)
}
if _, err := kv.Put(ctx, key, stored); err != nil {
return false, fmt.Errorf("the word of %s cannot be kept: %w", s.Module, err)
}
fmt.Printf("states: %s keeps %s\n", s.Module, statesWords(s))
return true, nil
}
// statesWords is an announcement as a person reads it: each state, readable ones marked.
func statesWords(s announce.States) string {
if len(s.States) == 0 {
return "nothing"
}
var out []string
for _, k := range s.States {
if k.Readable {
out = append(out, k.Name+" (readable)")
} else {
out = append(out, k.Name)
}
}
return strings.Join(out, ", ")
}
// readAnnouncements is the last word of every module, by module (what inventory.Announcements reads): none when the
// bucket is not there yet, which reads every owner as not yet announced.
func readAnnouncements(ctx context.Context) (map[string]announce.States, error) {
js, err := aBus()
if err != nil {
return nil, err
}
defer js.Close()
api, err := jetstream.New(js.Conn())
if err != nil {
return nil, err
}
kv, err := api.KeyValue(ctx, broker.StatesAnnouncedBucket)
if errors.Is(err, jetstream.ErrBucketNotFound) {
// empty-on-error: not an error; no module announced yet, so every read is granted from `reads` as before
return map[string]announce.States{}, nil
}
if err != nil {
return nil, err
}
keys, err := kv.Keys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
// empty-on-error: not an error; no module announced yet
return map[string]announce.States{}, nil
}
if err != nil {
return nil, err
}
out := map[string]announce.States{}
for _, key := range keys {
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
continue
}
if err != nil {
return nil, err
}
s, err := announce.Decode(e.Value())
if err != nil {
return nil, fmt.Errorf("the kept word of %s is not read: %w", key, err)
}
out[s.Module] = s
}
return out, nil
}
// composeAnnouncedReads sends the machine holding the bus its user list when a changed word changed it (D3): as a new
// controller's user list is carried (carryUserList), not when that machine is held.
func composeAnnouncedReads(ctx context.Context, open *stores) {
holder, behind, err := brokerBehind(ctx, open, nil)
if err != nil {
fmt.Printf("states: whether the bus's user list is behind the states announced cannot be read: %v\n", err)
return
}
if holder == "" || !behind {
return
}
held, err := heldMachines(ctx, open, []string{holder})
if err != nil {
fmt.Printf("states: whether %s may be sent its new user list cannot be read: %v\n", holder, err)
return
}
if why, isHeld := held[holder]; isHeld {
fmt.Printf("states: the user list the states announced compose is not sent to %s, which holds the bus: %s — "+
"`push %s` sends it\n", holder, strings.Join(why, "; "), holder)
return
}
if _, err := sendRollout(ctx, open, []string{holder}); err != nil {
fmt.Printf("states: the user list the states announced compose could not be sent to %s: %v\n", holder, err)
return
}
fmt.Printf("states: sent %s, which holds the bus, the user list the states announced compose\n", holder)
}
// notYetAnnounced are the owners some module reads the state of and that have announced nothing yet: what `status`
// names while the transition lasts.
func notYetAnnounced(ctx context.Context, inv *inventory.Inventory) ([]string, error) {
if inventory.Announcements == nil {
return nil, nil
}
records, err := inv.BusRecords(ctx)
if err != nil {
return nil, err
}
if records.Announced == nil {
return nil, errors.New("the states announced cannot be read")
}
return broker.NotAnnounced(records), nil
}