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 }