219 lines
7.2 KiB
Go
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
|
|
}
|