Files
mesh-delivery/modules/mesh-delivery-view/internal/view/status.go
T

219 lines
7.2 KiB
Go

package view
import (
"encoding/json"
"fmt"
"sort"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
)
// `status` is the view's derivation (novox/hq ADR 0564 §10, Limits 7, issue 1952 V1): what is wrong, quiet and on its way,
// read from the buckets it watches, never from a summary someone writes (research 072 §2), so the controller's status, its
// summary and the probe D9 go with its lease. Each part comes from one bucket:
// - Conditions: the condition keeper's open conditions (mesh-conditions.conditions), urgent first, then oldest;
// - Quiet: the watchman's presence of each node that is offline, and its stuck keys not moved;
// - Moving: each module whose want and have differ, as mesh-sdk's delivery.MotionOf derives it (moving or stuck);
// - Heals: what mesh-healers did in the last seven days (mesh-healers.attempts), by outcome.
// A bucket that cannot be read says so in its part's Unread, never as none.
// StatusTool is the view's tool that answers status.
const StatusTool = "view_status"
// The states status reads, as the view's manifest names them.
const (
StatusConditionsState = health.KeeperModule + "." + health.ConditionsState
StatusAttemptsState = health.HealersModule + "." + health.AttemptsState
StatusPresenceState = delivery.WatchmanModule + ".presence"
StatusStuckState = delivery.WatchmanModule + ".stuck"
)
// HealsWithin is how far back status counts the healers' acts.
const HealsWithin = 7 * 24 * time.Hour
// StatusRead is the five buckets status reads, as a tool reads them.
type StatusRead struct {
Conditions, Attempts, Presence, Stuck, Wants, Haves StateRead
}
// Status is the view's answer to status.
type Status struct {
Conditions []StatusCondition `json:"conditions"`
ConditionsUnread string `json:"conditions-unread,omitempty"`
Quiet []StatusQuiet `json:"quiet"`
QuietUnread string `json:"quiet-unread,omitempty"`
Moving []StatusMoving `json:"moving"`
MovingUnread string `json:"moving-unread,omitempty"`
Heals map[string]int `json:"heals-this-week"`
HealsUnread string `json:"heals-unread,omitempty"`
// Well says nothing is open, quiet or stuck; a part unread is not well.
Well bool `json:"well"`
}
// StatusCondition is one open condition in brief.
type StatusCondition struct {
Key string `json:"key"`
Severity string `json:"severity"`
Machine string `json:"machine,omitempty"`
Summary string `json:"summary"`
Raised time.Time `json:"raised"`
Silenced bool `json:"silenced,omitempty"`
}
// StatusQuiet is a node offline, or a key the watchman says stopped moving.
type StatusQuiet struct {
Key string `json:"key"`
What string `json:"what"`
Since time.Time `json:"since,omitzero"`
}
// StatusMoving is a module whose want and have differ on a node.
type StatusMoving struct {
Node string `json:"node"`
Module string `json:"module"`
State string `json:"state"`
Outcome string `json:"outcome,omitempty"`
Words string `json:"words,omitempty"`
FromFP string `json:"from_fp,omitempty"`
ToFP string `json:"to_fp"`
}
func values(s StateRead) (map[string]json.RawMessage, error) {
if s.Keys == nil || s.Get == nil {
return nil, fmt.Errorf("not read here")
}
keys, err := s.Keys()
if err != nil {
return nil, err
}
out := map[string]json.RawMessage{}
for _, k := range keys {
v, err := s.Get(k)
if err != nil {
return nil, fmt.Errorf("%s: %w", k, err)
}
if v != nil {
out[k] = v
}
}
return out, nil
}
// DeriveStatus is status from the buckets r reads, at now.
func DeriveStatus(r StatusRead, now time.Time) Status {
st := Status{Conditions: []StatusCondition{}, Quiet: []StatusQuiet{}, Moving: []StatusMoving{}, Heals: map[string]int{}}
if raw, err := values(r.Conditions); err != nil {
st.ConditionsUnread = "the condition keeper's conditions cannot be read: " + err.Error()
} else {
var open []health.Condition
for _, v := range raw {
var c health.Condition
if json.Unmarshal(v, &c) == nil && c.Key != "" {
open = append(open, c)
}
}
health.Order(open)
for _, c := range open {
st.Conditions = append(st.Conditions, StatusCondition{Key: c.Key, Severity: string(c.Severity), Machine: c.Subject.Machine,
Summary: c.Summary, Raised: c.Raised, Silenced: c.SilencedAt(now)})
}
}
presence, perr := values(r.Presence)
stuck, serr := values(r.Stuck)
switch {
case perr != nil:
st.QuietUnread = "the watchman's presence cannot be read: " + perr.Error()
case serr != nil:
st.QuietUnread = "the watchman's stuck keys cannot be read: " + serr.Error()
default:
for k, v := range presence {
if p, err := delivery.DecodePresence(v); err == nil && !p.Online {
st.Quiet = append(st.Quiet, StatusQuiet{Key: k, What: "offline"})
}
}
for k, v := range stuck {
if s, err := delivery.DecodeStuck(v); err == nil && !s.Moved {
st.Quiet = append(st.Quiet, StatusQuiet{Key: k, What: "stopped moving: it waited for " + s.Waited, Since: s.Since})
}
}
sort.Slice(st.Quiet, func(i, j int) bool { return st.Quiet[i].Key < st.Quiet[j].Key })
}
wants, werr := values(r.Wants)
haves, herr := values(r.Haves)
switch {
case werr != nil:
st.MovingUnread = "the assigner's wants cannot be read: " + werr.Error()
case herr != nil:
st.MovingUnread = "the nodes' haves cannot be read: " + herr.Error()
default:
for wk, v := range wants {
node, module, err := delivery.ParseWantKey(wk)
if err != nil {
continue
}
w, err := delivery.DecodeWant(v)
if err != nil {
continue
}
hk, _ := delivery.HaveKey(node, module)
var h delivery.Have
hv, has := haves[hk]
if has {
if h, err = delivery.DecodeHave(hv); err != nil {
has = false
}
}
words, stopped := "", false
if sk, err := delivery.StuckKey(delivery.AssignerWantsBucket, wk); err == nil && serr == nil {
if s, err := delivery.DecodeStuck(stuck[sk]); err == nil && !s.Moved {
words, stopped = s.Waited, true
}
}
m := delivery.MotionOf(w, true, h, has, words, stopped)
if m.State == "" || m.State == delivery.MotionSettled {
continue
}
row := StatusMoving{Node: node, Module: module, State: m.State, Outcome: m.Outcome, Words: m.Words, ToFP: w.FP}
if has {
row.FromFP = h.FP
}
st.Moving = append(st.Moving, row)
}
sort.Slice(st.Moving, func(i, j int) bool {
if st.Moving[i].Node != st.Moving[j].Node {
return st.Moving[i].Node < st.Moving[j].Node
}
return st.Moving[i].Module < st.Moving[j].Module
})
}
if raw, err := values(r.Attempts); err != nil {
st.HealsUnread = "mesh-healers' attempts cannot be read: " + err.Error()
} else {
for _, v := range raw {
var a health.Attempt
if json.Unmarshal(v, &a) != nil || now.Sub(a.Begun) > HealsWithin {
continue
}
outcome := a.Outcome
if outcome == "" {
outcome = health.OutcomeActing
}
st.Heals[outcome]++
}
}
stuckMoving := false
for _, m := range st.Moving {
stuckMoving = stuckMoving || m.State == delivery.MotionStuck
}
st.Well = len(st.Conditions) == 0 && len(st.Quiet) == 0 && !stuckMoving &&
st.ConditionsUnread == "" && st.QuietUnread == "" && st.MovingUnread == ""
return st
}