Every one of the 48 core failures of research 031 was found by a person looking; the mesh's answers carried the fact for whoever asked and told nobody. - The condition store (to-be 45 §2): mesh-controller_conditions, one key per open condition, written by compare-and-set so a person's silence and the watchdogs never lose each other's word; every transition kept ninety days in mesh-controller_condition-history and said as the seat's events condition-raised / condition-changed / condition-cleared (the condition at the top level, with event, at, change, why, show), offered again while the bus is away. Raised and cleared by observation only; a clearing reopened within ten minutes is the same condition with its count up, its silence kept. Verbs: conditions, conditions show, conditions silence (a hand act, at most a week), conditions history. - ADR 0224's provider standing is the first kind, provider-failing, held by the provider's events; the provider_standing table is no longer read or written (left in place: dropping it is the operator's word). - status leads with the open conditions, urgent first, and says all well only with none open; conditions it cannot read are said and not well. - The signals table compiled in, one watchdog loop over it every 30s: S1 heartbeat (3 intervals, asleep machines excepted, control node urgent after 30 min), S2 report after a send, S3 plan tier, S4 event loop deaf, S5 merge not acted, S6 ask lost, S7 call hung, S8 provider silent, S9 advisories, S10 self-check silent, S11 node tools silent, S13 stale refusals; S12, S14, S15 deferred with their reasons. A row that cannot see raises probe-failed and clears nothing. A test generated from the table suppresses each signal inside and past its bound. - The bus's advisories (maximum deliveries, a mesh consumer deleted) and the controller's own slow consumer and refused subjects, said in the mesh's words. - doctor: the probe registry D1-D10 (D5 deferred) and DW, every five minutes, each in thirty seconds; a probe that cannot run is never a pass. D1 validates with mesh-host's own validator. Every run ends with the doctor-heartbeat event mesh-watcher listens for. - The controller is granted its new buckets, events, the two advisories and $SRV.INFO; the node tools their tools-alive heartbeat. The streams and consumers the controller asserts and the ones D6/D7 expect are one derivation.
160 lines
4.7 KiB
Go
160 lines
4.7 KiB
Go
package conditions
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// The condition store on the bus (to-be 45 §2, ADR 0201): the controller's two buckets, asserted at
|
|
// its start like its calls and its hand-act log.
|
|
|
|
// OnTheBus opens the store and its history on a connection.
|
|
func OnTheBus(ctx context.Context, conn *nats.Conn) (Backend, History, error) {
|
|
api, err := jetstream.New(conn)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
open, err := api.KeyValue(ctx, broker.ConditionsBucket)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("the condition store %s is not on the bus — the controller asserts it at "+
|
|
"its start, so one older than this has not: %w", broker.ConditionsBucket, err)
|
|
}
|
|
history, err := api.KeyValue(ctx, broker.ConditionHistoryBucket)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("the condition history %s is not on the bus — the controller asserts it "+
|
|
"at its start, so one older than this has not: %w", broker.ConditionHistoryBucket, err)
|
|
}
|
|
return busStore{open}, &busHistory{api: api, kv: history}, nil
|
|
}
|
|
|
|
type busStore struct{ kv jetstream.KeyValue }
|
|
|
|
func (b busStore) Get(ctx context.Context, key string) (Entry, bool, error) {
|
|
e, err := b.kv.Get(ctx, key)
|
|
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
|
return Entry{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return Entry{}, false, err
|
|
}
|
|
return Entry{Value: e.Value(), Revision: e.Revision()}, true, nil
|
|
}
|
|
|
|
func (b busStore) Create(ctx context.Context, key string, value []byte) error {
|
|
_, err := b.kv.Create(ctx, key, value)
|
|
if errors.Is(err, jetstream.ErrKeyExists) {
|
|
return ErrMoved
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b busStore) Update(ctx context.Context, key string, value []byte, revision uint64) error {
|
|
_, err := b.kv.Update(ctx, key, value, revision)
|
|
return moved(err)
|
|
}
|
|
|
|
func (b busStore) Delete(ctx context.Context, key string, revision uint64) error {
|
|
return moved(b.kv.Delete(ctx, key, jetstream.LastRevision(revision)))
|
|
}
|
|
|
|
// moved reads the server's refusal of a compare-and-set as what it is.
|
|
func moved(err error) error {
|
|
var apiErr *jetstream.APIError
|
|
if errors.As(err, &apiErr) && apiErr.ErrorCode == jetstream.JSErrCodeStreamWrongLastSequence {
|
|
return ErrMoved
|
|
}
|
|
return err
|
|
}
|
|
|
|
// All is every key, read through a watch that hands over each current value and then says it has.
|
|
func (b busStore) All(ctx context.Context) (map[string]Entry, error) {
|
|
w, err := b.kv.WatchAll(ctx, jetstream.IgnoreDeletes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = w.Stop() }()
|
|
out := map[string]Entry{}
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("reading the condition store: %w", ctx.Err())
|
|
case e := <-w.Updates():
|
|
if e == nil {
|
|
return out, nil
|
|
}
|
|
out[e.Key()] = Entry{Value: e.Value(), Revision: e.Revision()}
|
|
}
|
|
}
|
|
}
|
|
|
|
// busHistory keeps each transition under a key of its time and a sequence, and reads them back
|
|
// from a moment through the stream under the bucket — by time, so a read of the last ten minutes
|
|
// does not read ninety days.
|
|
type busHistory struct {
|
|
api jetstream.JetStream
|
|
kv jetstream.KeyValue
|
|
seq atomic.Uint64
|
|
}
|
|
|
|
func (h *busHistory) Append(ctx context.Context, e Event) error {
|
|
body, err := json.Marshal(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key := strconv.FormatInt(e.At.UnixNano(), 10) + "-" + strconv.FormatUint(h.seq.Add(1), 10)
|
|
_, err = h.kv.Put(ctx, key, body)
|
|
return err
|
|
}
|
|
|
|
// historyQuiet is how long a read of the history waits for one more transition before it takes the
|
|
// stream as read to its end; it answers at once while it holds something.
|
|
const historyQuiet = 2 * time.Second
|
|
|
|
func (h *busHistory) Since(ctx context.Context, since time.Time) ([]Event, error) {
|
|
start := since
|
|
consumer, err := h.api.OrderedConsumer(ctx, "KV_"+broker.ConditionHistoryBucket, jetstream.OrderedConsumerConfig{
|
|
DeliverPolicy: jetstream.DeliverByStartTimePolicy, OptStartTime: &start,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading the condition history: %w", err)
|
|
}
|
|
info, err := consumer.Info(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading the condition history: %w", err)
|
|
}
|
|
var out []Event
|
|
pending := info.NumPending
|
|
for pending > 0 {
|
|
msg, err := consumer.Next(jetstream.FetchMaxWait(historyQuiet))
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
// Nothing more within the quiet wait: read to its end.
|
|
break
|
|
}
|
|
meta, err := msg.Metadata()
|
|
if err != nil {
|
|
break
|
|
}
|
|
pending = meta.NumPending
|
|
var e Event
|
|
if len(msg.Data()) > 0 && json.Unmarshal(msg.Data(), &e) == nil && e.Key != "" {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
|
|
return out, nil
|
|
}
|