View: answer status from the buckets it reads and derive motion with mesh-sdk's MotionOf, so status leaves the controller and the keeper judges a delivery as the view does (issue 1952 V1)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mesh-delivery-view/internal/view"
|
||||
|
||||
@@ -19,6 +20,14 @@ import (
|
||||
// Tools are the read tools over a modules state, and build_usage over the build modules' states.
|
||||
func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
|
||||
return []stdio.Tool{
|
||||
{Name: view.StatusTool,
|
||||
Description: "The mesh's status, derived by the view from the buckets it reads (novox/hq ADR 0564 §10), nothing written as a " +
|
||||
"summary: every open condition of the condition keeper (mesh-conditions), urgent first then oldest; what is quiet (each " +
|
||||
"node the watchman says is offline, each key it says stopped moving); each module whose want and have differ on a node " +
|
||||
"(moving, or stuck with the have's or the watchman's words), as mesh-sdk's delivery motion derives it; and what " +
|
||||
"mesh-healers did in the last seven days, by outcome. A part that cannot be read says so, never as none; well is true " +
|
||||
"only when nothing is open, quiet or stuck and every part was read. Read-only.",
|
||||
Run: func(map[string]any) (any, error) { return r.Status(time.Now()), nil }},
|
||||
{Name: view.ReachReadTool,
|
||||
Description: "Whether a merge is on the machines: per repository, the reached head (the newest merge every node runs a build " +
|
||||
"containing, as the view derives it from the indexer's modules, the wants and the haves; a merge at or before it is on the " +
|
||||
@@ -232,9 +241,11 @@ func read(state stdio.KeptState) view.StateRead {
|
||||
func main() {
|
||||
r := view.Reader{State: read(stdio.State("mesh-delivery-indexer.modules")), Builds: read(stdio.State("mesh-delivery-builder.builds")), CandidatesState: read(stdio.State("mesh-delivery-indexer.candidates")),
|
||||
WantsState: read(stdio.State("mesh-delivery-assigner.wants")), DifferencesState: read(stdio.State("mesh-delivery-assigner.differences")), HavesState: read(stdio.State("node-engine.have")),
|
||||
ReachState: read(stdio.State("reach")),
|
||||
HeardState: read(stdio.State("mesh-delivery-indexer.heard")),
|
||||
SignersState: read(stdio.State("mesh-vault.signers"))}
|
||||
ReachState: read(stdio.State("reach")),
|
||||
HeardState: read(stdio.State("mesh-delivery-indexer.heard")),
|
||||
SignersState: read(stdio.State("mesh-vault.signers")),
|
||||
ConditionsState: read(stdio.State(view.StatusConditionsState)), AttemptsState: read(stdio.State(view.StatusAttemptsState)),
|
||||
PresenceState: read(stdio.State(view.StatusPresenceState)), StuckState: read(stdio.State(view.StatusStuckState))}
|
||||
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
|
||||
Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())}
|
||||
for _, lang := range delivery.AskLanguages {
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-view
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a h1:nhRFmtdcI4VkghGUdJhpt1BnPuTQcekioOIRzW8fcG0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba h1:uXwkcQWgbu/UthizGHFaKPiA4NBVJgqL9q2jJnnGR1o=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -46,6 +47,15 @@ type Reader struct {
|
||||
HeardState StateRead
|
||||
// SignersState is the vault's public signing halves (ADR 0550 §2); unset reads none.
|
||||
SignersState StateRead
|
||||
// ConditionsState, AttemptsState, PresenceState and StuckState are what status reads beside the wants and the haves
|
||||
// (ADR 0564 §10): the condition keeper's open conditions, mesh-healers' attempts, the watchman's presence and stuck keys.
|
||||
ConditionsState, AttemptsState, PresenceState, StuckState StateRead
|
||||
}
|
||||
|
||||
// Status is the view's derivation of status at now (status.go).
|
||||
func (r Reader) Status(now time.Time) Status {
|
||||
return DeriveStatus(StatusRead{Conditions: r.ConditionsState, Attempts: r.AttemptsState, Presence: r.PresenceState,
|
||||
Stuck: r.StuckState, Wants: r.WantsState, Haves: r.HavesState}, now)
|
||||
}
|
||||
|
||||
// ReachReadTool reads the view's reach: per repository, how far each module reached and the reached head (ADR 0556).
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
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
|
||||
}
|
||||
@@ -1185,9 +1185,9 @@ func splitKey(key string) (node, module string) {
|
||||
// module's want on a node differs from its have (moving), that the have reached the want running and healthy (settled), or that it
|
||||
// is held or failed or stopped moving past the watchman's bound (stuck). Each is said once for a node, a module and a composition.
|
||||
const (
|
||||
MotionMoving = "moving"
|
||||
MotionSettled = "settled"
|
||||
MotionStuck = "stuck"
|
||||
MotionMoving = delivery.MotionMoving
|
||||
MotionSettled = delivery.MotionSettled
|
||||
MotionStuck = delivery.MotionStuck
|
||||
// MotionBoundSeconds is how long a node may take to apply a want before the watchman says it stuck (watch.StepBound).
|
||||
MotionBoundSeconds = 1800
|
||||
)
|
||||
@@ -1316,7 +1316,6 @@ func (m *Model) motionOf(hk string) []MotionEvent {
|
||||
id := func(kind string) string {
|
||||
return kind + "." + node + "." + module + "." + strings.TrimPrefix(w.value.Composed, "sha256:")
|
||||
}
|
||||
applied := has && h.have.FP == w.value.FP && (h.have.Composed == "" || h.have.Composed == w.value.Composed)
|
||||
var out []MotionEvent
|
||||
say := func(kind string, b MotionBody) {
|
||||
if m.told[id(kind)] {
|
||||
@@ -1325,31 +1324,25 @@ func (m *Model) motionOf(hk string) []MotionEvent {
|
||||
m.told[id(kind)] = true
|
||||
out = append(out, MotionEvent{Type: kind, ID: id(kind), Body: b})
|
||||
}
|
||||
switch {
|
||||
case applied && h.have.Phase == delivery.PhaseRunning && h.have.Health == delivery.HealthOK:
|
||||
// The derivation is mesh-sdk's (delivery.MotionOf, issue 1952 S7), shared with the condition keeper's hold.
|
||||
words, stuck := m.stuckWant(w.value, node, module)
|
||||
mo := delivery.MotionOf(w.value, true, h.have, has, words, stuck)
|
||||
if mo.State == delivery.MotionSettled {
|
||||
if m.told[id(MotionMoving)] {
|
||||
b := body
|
||||
b.Outcome = "running"
|
||||
b.Outcome = mo.Outcome
|
||||
say(MotionSettled, b)
|
||||
}
|
||||
return out
|
||||
case !m.told[id(MotionMoving)]:
|
||||
}
|
||||
if !m.told[id(MotionMoving)] {
|
||||
say(MotionMoving, body)
|
||||
}
|
||||
stop := body
|
||||
switch {
|
||||
case has && h.have.Phase == delivery.PhaseHeld:
|
||||
stop.Outcome, stop.Words = "stopped", h.have.Detail
|
||||
case has && (h.have.Health == delivery.HealthFailed || h.have.Phase == delivery.PhaseFailed) && h.have.FP == w.value.FP:
|
||||
stop.Outcome, stop.Words = "failed", h.have.Detail
|
||||
default:
|
||||
if s, ok := m.stuckWant(w.value, node, module); ok {
|
||||
stop.Outcome, stop.Words = "stopped", s
|
||||
} else {
|
||||
return out
|
||||
}
|
||||
if mo.State == delivery.MotionStuck {
|
||||
stop := body
|
||||
stop.Outcome, stop.Words = mo.Outcome, mo.Words
|
||||
say(MotionStuck, stop)
|
||||
}
|
||||
say(MotionStuck, stop)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
var t0 = time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC)
|
||||
@@ -212,7 +214,13 @@ func stuckE(t *testing.T, bucket, key, waited string, since time.Time, moved boo
|
||||
return Entry{Bucket: delivery.StuckBucket, Key: bucket + "." + key, Value: b, At: since}
|
||||
}
|
||||
|
||||
func TestOfflineAndStuckAreListed(t *testing.T) { // 1168/AC2
|
||||
func TestTheWatchmansKeysAndStatus(t *testing.T) {
|
||||
t.Run("offline and stuck are listed", offlineAndStuckAreListed)
|
||||
t.Run("an unknown watchman version is unreadable, not dropped", unknownWatchmanVersionIsUnreadableNotDropped)
|
||||
t.Run("1952/V1: status derives each part from its bucket, and says a part it cannot read", statusDerivesEachPartFromItsBucket)
|
||||
}
|
||||
|
||||
func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2
|
||||
m := New()
|
||||
fill(m, base(t)...)
|
||||
fill(m,
|
||||
@@ -240,7 +248,7 @@ func TestOfflineAndStuckAreListed(t *testing.T) { // 1168/AC2
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3
|
||||
func unknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3
|
||||
m := New()
|
||||
fill(m,
|
||||
Entry{Bucket: delivery.PresenceBucket, Key: "c", Value: []byte(`{"v":9,"online":false}`), At: t0},
|
||||
@@ -267,3 +275,70 @@ func TestUnknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC
|
||||
t.Fatal("a readable value must clear the unreadable mark")
|
||||
}
|
||||
}
|
||||
|
||||
// 1952/V1 (ADR 0564 §10): status reads the keeper's open conditions, the watchman's presence and stuck keys, the wants against
|
||||
// the haves and the healers' attempts of the week; nothing writes a summary, and a part it cannot read is said, never well.
|
||||
func statusDerivesEachPartFromItsBucket(t *testing.T) {
|
||||
now := t0.Add(time.Hour)
|
||||
composedC := "sha256:" + strings.Repeat("c", 64)
|
||||
fp1, fp2, fp3 := delivery.SourcePrefix2+strings.Repeat("1", 64), delivery.SourcePrefix2+strings.Repeat("2", 64), delivery.SourcePrefix2+strings.Repeat("3", 64)
|
||||
enc := func(v any) string { b, _ := json.Marshal(v); return string(b) }
|
||||
want := func(fp string) string {
|
||||
b, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: composedC})
|
||||
return string(b)
|
||||
}
|
||||
have := func(fp string, phase delivery.Phase, health delivery.Health, detail string) string {
|
||||
b, err := delivery.EncodeHave(delivery.Have{FP: fp, Composed: composedC, Phase: phase, Health: health, Detail: detail})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
presence := func(on bool) string { b, _ := delivery.EncodePresence(delivery.Presence{Online: on}); return string(b) }
|
||||
stuckOf := func(bucket, key, waited string, moved bool) string {
|
||||
b, _ := delivery.EncodeStuck(delivery.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: t0, Moved: moved})
|
||||
return string(b)
|
||||
}
|
||||
stuckWant, _ := delivery.StuckKey(delivery.AssignerWantsBucket, "want.b.web")
|
||||
fin := t0.Add(time.Minute)
|
||||
r := StatusRead{
|
||||
Conditions: states(map[string]string{
|
||||
"machine.b.silent": enc(health.Condition{Key: "machine.b.silent", Severity: health.Warning, Summary: "b silent", Raised: t0}),
|
||||
"core.doctor.self-check": enc(health.Condition{Key: "core.doctor.self-check", Severity: health.Urgent, Summary: "no run", Raised: t0.Add(time.Minute)}),
|
||||
}),
|
||||
Presence: states(map[string]string{"a": presence(true), "b": presence(false)}),
|
||||
Stuck: states(map[string]string{stuckWant: stuckOf(delivery.AssignerWantsBucket, "want.b.web", "b to run web", false),
|
||||
"x.moved": stuckOf("x", "moved", "y", true)}),
|
||||
Wants: states(map[string]string{"want.a.web": want(fp2), "want.a.mail": want(fp1), "want.b.web": want(fp2), "want.a.db": want(fp3)}),
|
||||
Haves: states(map[string]string{"a.web": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), "a.mail": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""),
|
||||
"b.web": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), "a.db": have(fp3, delivery.PhaseRunning, delivery.HealthFailed, "the unit failed")}),
|
||||
Attempts: states(map[string]string{"1": enc(health.Attempt{Outcome: health.OutcomeActed, Begun: t0, Finished: &fin}),
|
||||
"2": enc(health.Attempt{Outcome: health.OutcomeEscalated, Begun: t0}), "old": enc(health.Attempt{Outcome: health.OutcomeActed, Begun: now.Add(-8 * 24 * time.Hour)})}),
|
||||
}
|
||||
st := DeriveStatus(r, now)
|
||||
if len(st.Conditions) != 2 || st.Conditions[0].Key != "core.doctor.self-check" || st.Conditions[1].Key != "machine.b.silent" {
|
||||
t.Errorf("conditions = %+v, want the urgent one first", st.Conditions)
|
||||
}
|
||||
if len(st.Quiet) != 2 || st.Quiet[0].Key != "b" || st.Quiet[1].Key != stuckWant {
|
||||
t.Errorf("quiet = %+v", st.Quiet)
|
||||
}
|
||||
wantMoving := []StatusMoving{
|
||||
{Node: "a", Module: "db", State: delivery.MotionStuck, Outcome: delivery.OutcomeFailed, Words: "the unit failed", FromFP: fp3, ToFP: fp3},
|
||||
{Node: "a", Module: "web", State: delivery.MotionMoving, FromFP: fp1, ToFP: fp2},
|
||||
{Node: "b", Module: "web", State: delivery.MotionStuck, Outcome: delivery.OutcomeStopped, Words: "b to run web", FromFP: fp1, ToFP: fp2},
|
||||
}
|
||||
if !reflect.DeepEqual(st.Moving, wantMoving) {
|
||||
t.Errorf("moving = %+v\nwant %+v", st.Moving, wantMoving)
|
||||
}
|
||||
if !reflect.DeepEqual(st.Heals, map[string]int{health.OutcomeActed: 1, health.OutcomeEscalated: 1}) || st.Well {
|
||||
t.Errorf("heals = %v, well %v", st.Heals, st.Well)
|
||||
}
|
||||
// A part it cannot read is said, and the mesh is not well; with nothing wrong and every part read it is.
|
||||
if st := DeriveStatus(StatusRead{}, now); st.ConditionsUnread == "" || st.QuietUnread == "" || st.MovingUnread == "" || st.HealsUnread == "" || st.Well {
|
||||
t.Errorf("nothing read = %+v", st)
|
||||
}
|
||||
empty := states(map[string]string{})
|
||||
if st := DeriveStatus(StatusRead{Conditions: empty, Presence: empty, Stuck: empty, Wants: empty, Haves: empty, Attempts: empty}, now); !st.Well {
|
||||
t.Errorf("an empty mesh = %+v, want well", st)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
if m.Module != "mesh-delivery-view" {
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-config.resolved", "mesh-controller.builds", "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-vault.signers", "node-engine.have"} // build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-controller.builds", "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-vault.signers", "node-engine.have"} // view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -119,7 +119,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
}
|
||||
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
|
||||
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
|
||||
if want := []string{"indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "builder_build", "builder_builds", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "builder_build", "builder_builds", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
for _, a := range m.Build.Artifacts {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"moved": true,
|
||||
"slug": "view",
|
||||
"tools": [
|
||||
"view_status",
|
||||
"indexer_module",
|
||||
"indexer_modules",
|
||||
"indexer_candidate",
|
||||
@@ -53,6 +54,8 @@
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.taken",
|
||||
"mesh-delivery-teller.answers",
|
||||
"mesh-conditions.conditions",
|
||||
"mesh-healers.attempts",
|
||||
"mesh-delivery-watchman.presence",
|
||||
"mesh-delivery-watchman.stuck",
|
||||
"mesh-vault.signers",
|
||||
|
||||
Reference in New Issue
Block a user