Files
mesh-delivery/modules/mesh-delivery-view/internal/view/status.go
T
jschoubben 8afd34525d DLV-M4, DLV-V1: the view reads the indexer's and the assigner's twelve states through their read tools, never their buckets
ADR 0615 §1, design 25 § 12.2: the manifest drops its twelve reads of the two owners' states and invokes
their twelve read tools. The process follows the six it watches (placement, wants, asks, candidates,
modules, taken) by the first view and then the views families (consumes), through firstview pictures, so
its watch code is unchanged; an owner that is down holds no other watch, and the unanswered first view is
the view's finding (states first-views and findings; tools view_first_views, view_first_view_again).
The tools keep their names, arguments and answers and ask the owners' read tools on each call
(view.OwnerRead; a read of many keys is one paged values read). DLV-V1: a table subtest answers each
tool through the owner's own Read over a fake state and matches what reading the state answered.

The owners' read tools answer no history of a key: indexer_module_history, indexer_heard and view_tiers'
times say so (an error, never none) until the read tool takes a history argument (a request to the join).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:24:17 +01:00

222 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.All != nil {
return s.All()
}
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
}