mesh-conditions keeper works over the seat's types: conditions, events and findings in the seat's layout, the old layout still read (B5e; ADR 0634 §1, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -20,7 +20,9 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
)
|
||||
|
||||
// The module whose delivery holds what it serves beyond their own findings (Limits 12).
|
||||
@@ -32,19 +34,19 @@ type about struct{ Node, Module string }
|
||||
// aboutOf are the modules on nodes a condition's subject is about (Limits 12): a module on a machine, a provider on its
|
||||
// node, a seat of the same name as its module on a machine, and every module's tools on a machine whose tool
|
||||
// runner is delivered. Never the machine itself.
|
||||
func (k *Keeper) aboutOf(kind, source string, s health.Subject) []about {
|
||||
func (k *Keeper) aboutOf(kind, source string, s seat.Subject) []about {
|
||||
var out []about
|
||||
parts := strings.Split(s.ID, ".")
|
||||
switch s.Scope {
|
||||
case health.ScopeModule:
|
||||
case cc.ScopeModule:
|
||||
if len(parts) >= 2 {
|
||||
out = append(out, about{Node: parts[len(parts)-1], Module: strings.Join(parts[:len(parts)-1], ".")})
|
||||
}
|
||||
case health.ScopeProvider:
|
||||
case cc.ScopeProvider:
|
||||
if len(parts) >= 2 {
|
||||
out = append(out, about{Node: parts[1], Module: parts[0]})
|
||||
}
|
||||
case health.ScopeSeat:
|
||||
case cc.ScopeSeat:
|
||||
// The module the placement says holds the seat on that node, or the seat's own name where none is read.
|
||||
if len(parts) >= 2 {
|
||||
node, seat := parts[len(parts)-1], strings.Join(parts[:len(parts)-1], ".")
|
||||
@@ -54,7 +56,7 @@ func (k *Keeper) aboutOf(kind, source string, s health.Subject) []about {
|
||||
}
|
||||
out = append(out, about{Node: node, Module: module})
|
||||
}
|
||||
case health.ScopeMachine:
|
||||
case cc.ScopeMachine:
|
||||
// The machine itself (presence, network, resolver, disk) is never held; its tools are, while its tool runner is
|
||||
// delivered.
|
||||
if source == "S11" || kind == "tools-silent" {
|
||||
@@ -65,7 +67,7 @@ func (k *Keeper) aboutOf(kind, source string, s health.Subject) []about {
|
||||
}
|
||||
return out
|
||||
}
|
||||
if s.Machine != "" && s.Scope != health.ScopeMachine {
|
||||
if s.Machine != "" && s.Scope != cc.ScopeMachine {
|
||||
out = append(out, about{Node: s.Machine, Module: runtimeModule})
|
||||
}
|
||||
return out
|
||||
@@ -84,7 +86,7 @@ func (k *Keeper) motion(node, module string) (delivery.Motion, delivery.Want) {
|
||||
}
|
||||
|
||||
// holdFor says whether key is held now, and as what. Called with mu held.
|
||||
func (k *Keeper) holdFor(key string, f *health.Finding, open bool) (held, bool) {
|
||||
func (k *Keeper) holdFor(key string, f *seat.FindingsByKeyValue, open bool) (held, bool) {
|
||||
if open || f == nil {
|
||||
return held{}, false
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// The fingerprints and composition of mesh-controller on novox, copied from its have (mesh-delivery-view.node_haves,
|
||||
@@ -55,9 +55,9 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
// The delivery starts: the assigner wants the next fingerprint; the node applies it.
|
||||
_ = r.k.Want("want.novox.mesh-controller", want(ctrlNext, ctrlNextComp), false)
|
||||
_ = r.k.Have("novox.mesh-controller", have(ctrlNext, ctrlNextComp, delivery.PhaseApplying, delivery.HealthUnknown), false)
|
||||
own, ownRaw := finding(t, watchman, health.ScopeModule, "mesh-controller.novox", "unhealthy", "health", health.Warning, r.now)
|
||||
other, otherRaw := finding(t, watchman, health.ScopeModule, "postgres.novox", "unhealthy", "health", health.Warning, r.now)
|
||||
machine, machineRaw := finding(t, watchman, health.ScopeMachine, "novox", "silent", "S1", health.Urgent, r.now)
|
||||
own, ownRaw := finding(t, watchman, cc.ScopeModule, "mesh-controller.novox", "unhealthy", "health", cc.Warning, r.now)
|
||||
other, otherRaw := finding(t, watchman, cc.ScopeModule, "postgres.novox", "unhealthy", "health", cc.Warning, r.now)
|
||||
machine, machineRaw := finding(t, watchman, cc.ScopeMachine, "novox", "silent", "S1", cc.Urgent, r.now)
|
||||
for _, f := range []struct {
|
||||
key string
|
||||
raw []byte
|
||||
@@ -111,8 +111,8 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
r.k.Placement("ace.node", placed["ace.node"], false)
|
||||
_ = r.k.Want("want.ace.mesh-delivery-assigner", want(ctrlNext, ctrlNextComp), false)
|
||||
_ = r.k.Have("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, delivery.PhaseRunning, delivery.HealthOK), false)
|
||||
seat, seatRaw := finding(t, selfCheck, health.ScopeSeat, "mesh-module-assigner.ace", "holder-silent", "D3", health.Warning, r.now)
|
||||
_ = r.k.Finding(selfCheck, seat, seatRaw, false)
|
||||
seatKey, seatRaw := finding(t, selfCheck, cc.ScopeSeat, "mesh-module-assigner.ace", "holder-silent", "D3", cc.Warning, r.now)
|
||||
_ = r.k.Finding(selfCheck, seatKey, seatRaw, false)
|
||||
if got := r.changes(); got != "" || len(r.k.Held()) != 1 || r.k.Held()[0].Module != "mesh-delivery-assigner" {
|
||||
t.Errorf("a seat's finding while its holder is delivered: said %q, held %+v", got, r.k.Held())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ package main
|
||||
// the nodes' haves for the hold; nothing of the controller (its `conditions` are no longer read, issue 774, and its lease
|
||||
// turns nothing, issue 1126: the controller ended by its unassignment). From all of them it derives, per key, the
|
||||
// condition that should be open, and writes the difference by compare-and-set,
|
||||
// saying each transition as the controller says it (go/health) and keeping it in `history`. Nothing clears a condition
|
||||
// saying each transition as the seat's contract says it and keeping it in `history`. Nothing clears a condition
|
||||
// but its finding going (ADR 0231 §14).
|
||||
|
||||
import (
|
||||
@@ -19,15 +19,26 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
|
||||
)
|
||||
|
||||
// The modules whose states the keeper reads by name: the holders of the health seats (their seats' names are the
|
||||
// contracts'; the module is the holder).
|
||||
const (
|
||||
watchmanModule = "mesh-delivery-watchman"
|
||||
selfCheckModule = "mesh-self-check"
|
||||
healersModule = "mesh-healers"
|
||||
)
|
||||
|
||||
// The module, its seat, and the states it keeps.
|
||||
const (
|
||||
Module = health.KeeperModule
|
||||
Seat = "mesh-conditions"
|
||||
Module = "mesh-conditions"
|
||||
Seat = seat.Seat
|
||||
|
||||
ConditionsState = health.ConditionsState
|
||||
ConditionsState = seat.StateConditions
|
||||
HistoryState = "history"
|
||||
HeldState = "held"
|
||||
BoundsState = "bounds"
|
||||
@@ -39,7 +50,7 @@ const HistoryKeptFor = 90 * 24 * time.Hour
|
||||
// Observers are the modules whose `findings` the keeper reads (clause 2; ADR 0565 §8 for the assigner's; the lead's
|
||||
// table of 2026-10-29 for the provisioner's and the router's; ADR 0571 clause 2 for the seat artifact-store's holder,
|
||||
// which observes S25, image-missing).
|
||||
var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.HealersModule, delivery.AssignerModule,
|
||||
var Observers = []string{watchmanModule, selfCheckModule, healersModule, delivery.AssignerModule,
|
||||
"mesh-provisioner", "messenger", artifactStore, configModule, vaultModule,
|
||||
// The readers of the indexer's and the assigner's state say a first view unanswered past its bound in their own findings
|
||||
// (ADR 0615 §6).
|
||||
@@ -52,7 +63,7 @@ var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.H
|
||||
|
||||
// The other states it reads, as `<module>.<name>`.
|
||||
const (
|
||||
AttemptsRead = health.HealersModule + "." + health.AttemptsState
|
||||
AttemptsRead = healersModule + "." + meshhealers.StateAttempts
|
||||
WantsRead = delivery.AssignerModule + ".wants"
|
||||
// PlacementRead is the mesh-module-assigner seat's placement, read for which module holds a seat on a node (hold.go; the
|
||||
// lead, 2026-10-29, for ADR 0564 clause 2's inputs).
|
||||
@@ -87,16 +98,16 @@ type clearedMemo struct {
|
||||
at time.Time
|
||||
count int
|
||||
began time.Time
|
||||
gaps []health.Gap
|
||||
silenced *health.Silence
|
||||
tried []health.Tried
|
||||
gaps []Gap
|
||||
silenced *Silence
|
||||
tried []Tried
|
||||
}
|
||||
|
||||
// Keeper is the condition keeper's state and its writes.
|
||||
type Keeper struct {
|
||||
Store Store
|
||||
// Say publishes one event on the seat; History keeps one transition.
|
||||
Say func(e health.Event) error
|
||||
Say func(e Event) error
|
||||
Now func() time.Time
|
||||
Logf func(format string, a ...any)
|
||||
// Homes says, by kind, where each kind the controller raised lives now (homes.go); Homes when nil.
|
||||
@@ -105,9 +116,9 @@ type Keeper struct {
|
||||
Bound time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
findings map[string]map[string]health.Finding
|
||||
findings map[string]map[string]seat.FindingsByKeyValue
|
||||
blind map[string]string
|
||||
attempts map[string]health.Attempt
|
||||
attempts map[string]meshhealers.AttemptsByAttemptValue
|
||||
wants map[string]delivery.Want
|
||||
haves map[string]delivery.Have
|
||||
// holders are, by `<node>.<seat>`, the module the placement says holds the seat there.
|
||||
@@ -126,8 +137,8 @@ type Keeper struct {
|
||||
|
||||
func (k *Keeper) init() {
|
||||
if k.findings == nil {
|
||||
k.findings, k.blind, k.attempts = map[string]map[string]health.Finding{}, map[string]string{},
|
||||
map[string]health.Attempt{}
|
||||
k.findings, k.blind, k.attempts = map[string]map[string]seat.FindingsByKeyValue{}, map[string]string{},
|
||||
map[string]meshhealers.AttemptsByAttemptValue{}
|
||||
k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]delivery.Want{}, map[string]delivery.Have{},
|
||||
map[string]string{}, map[string]held{}, map[string]clearedMemo{}
|
||||
k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{}
|
||||
@@ -147,12 +158,12 @@ func (k *Keeper) Finding(observer, key string, value []byte, deleted bool) error
|
||||
k.mu.Lock()
|
||||
k.init()
|
||||
if k.findings[observer] == nil {
|
||||
k.findings[observer] = map[string]health.Finding{}
|
||||
k.findings[observer] = map[string]seat.FindingsByKeyValue{}
|
||||
}
|
||||
if deleted {
|
||||
delete(k.findings[observer], key)
|
||||
} else {
|
||||
f, err := health.DecodeFinding(value)
|
||||
f, err := decodeFinding(value)
|
||||
if err != nil {
|
||||
k.mu.Unlock()
|
||||
k.Logf("%s's finding %s is not one the keeper can raise, so it is left as it stood: %v", observer, key, err)
|
||||
@@ -194,7 +205,7 @@ func (k *Keeper) Attempt(key string, value []byte, deleted bool) error {
|
||||
if deleted {
|
||||
delete(k.attempts, key)
|
||||
} else {
|
||||
var a health.Attempt
|
||||
var a meshhealers.AttemptsByAttemptValue
|
||||
if err := json.Unmarshal(value, &a); err != nil || a.Condition == "" {
|
||||
k.mu.Unlock()
|
||||
k.Logf("the healers' attempt %s is not readable, so it is left out: %v", key, err)
|
||||
@@ -237,8 +248,8 @@ func (k *Keeper) EndCarried() (int, error) {
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
var was health.Condition
|
||||
if json.Unmarshal(raw, &was) != nil {
|
||||
was, err := decodeCondition(raw)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := k.clear(c.Key, was, rev, k.Now().UTC(), endedWhy); err != nil {
|
||||
@@ -281,7 +292,7 @@ func (k *Keeper) settleAll(keys []string) error {
|
||||
|
||||
// wanted is what the inputs say key should be: a finding of an observer. Nil when nothing says it; nothing is carried
|
||||
// from the controller's bucket (issue 1126).
|
||||
func (k *Keeper) wanted(key string) *health.Finding {
|
||||
func (k *Keeper) wanted(key string) *seat.FindingsByKeyValue {
|
||||
observers := make([]string, 0, len(k.findings))
|
||||
for o := range k.findings {
|
||||
observers = append(observers, o)
|
||||
@@ -302,9 +313,9 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading the condition %s: %w", key, err)
|
||||
}
|
||||
var was health.Condition
|
||||
var was Condition
|
||||
if open {
|
||||
if err := json.Unmarshal(raw, &was); err != nil {
|
||||
if was, err = decodeCondition(raw); err != nil {
|
||||
return fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
|
||||
}
|
||||
}
|
||||
@@ -331,9 +342,9 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
return nil
|
||||
}
|
||||
if open {
|
||||
_, err = k.Store.Update(ConditionsState, key, next, rev)
|
||||
_, err = k.Store.Update(ConditionsState, key, next.Stored(), rev)
|
||||
} else {
|
||||
_, err = k.Store.Create(ConditionsState, key, next)
|
||||
_, err = k.Store.Create(ConditionsState, key, next.Stored())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -342,29 +353,29 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
delete(k.cleared, key)
|
||||
k.mu.Unlock()
|
||||
for _, e := range events {
|
||||
e.Condition, e.At, e.Event = next, now, health.EventFor(e.Change)
|
||||
e.Show = health.ShowKeeper(next.Key)
|
||||
e.Condition, e.At, e.Event = next, now, cc.EventFor(e.Change)
|
||||
e.Show = cc.ShowKeeper(next.Key)
|
||||
k.tell(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// derive is the condition key should be now, from what was open and what the inputs say.
|
||||
func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.Finding, now time.Time) health.Condition {
|
||||
var c health.Condition
|
||||
func (k *Keeper) derive(key string, was Condition, open bool, f *seat.FindingsByKeyValue, now time.Time) Condition {
|
||||
var c Condition
|
||||
switch {
|
||||
case open:
|
||||
c = was
|
||||
k.fromFinding(&c, *f, now)
|
||||
default:
|
||||
c = health.Condition{Key: key, Raised: now, Count: 1, Resolver: health.ResolverSelf}
|
||||
c = Condition{Key: key, Raised: now, Count: 1, Resolver: cc.ResolverSelf}
|
||||
k.fromFinding(&c, *f, now)
|
||||
k.mu.Lock()
|
||||
if before, ok := k.cleared[key]; ok && now.Sub(before.at) <= health.ReopenWithin {
|
||||
if before, ok := k.cleared[key]; ok && now.Sub(before.at) <= cc.ReopenWithin {
|
||||
c.Count = before.count + 1
|
||||
c.First = before.began
|
||||
c.Gaps = append(append([]health.Gap(nil), before.gaps...), health.Gap{Cleared: before.at, Reopened: now})
|
||||
if over := len(c.Gaps) - health.KeptGaps; over > 0 {
|
||||
c.Gaps = append(append([]Gap(nil), before.gaps...), Gap{Cleared: before.at, Reopened: now})
|
||||
if over := len(c.Gaps) - cc.KeptGaps; over > 0 {
|
||||
c.First, c.Gaps = c.Gaps[over-1].Reopened, c.Gaps[over:]
|
||||
}
|
||||
if before.silenced != nil && now.Before(before.silenced.Until) {
|
||||
@@ -390,37 +401,37 @@ func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.F
|
||||
|
||||
// fromFinding is what the condition says of its finding: its words, subject, severity and source, and its evidence
|
||||
// newest first (clause 2). A healer's escalation is not undone by the finding saying a warning again.
|
||||
func (k *Keeper) fromFinding(c *health.Condition, f health.Finding, now time.Time) {
|
||||
func (k *Keeper) fromFinding(c *Condition, f seat.FindingsByKeyValue, now time.Time) {
|
||||
escalated := c.Escalated()
|
||||
c.Kind, c.Subject, c.Summary, c.Source = f.Kind, f.Subject, f.Summary, f.Source
|
||||
c.Headline, c.Explanation, c.Resolved, c.Needs, c.Actions = f.Headline, f.Explanation, f.Resolved, f.Needs, f.Actions
|
||||
if !(escalated && f.Severity != health.Urgent) {
|
||||
if !(escalated && f.Severity != cc.Urgent) {
|
||||
c.Severity = f.Severity
|
||||
}
|
||||
if !escalated {
|
||||
c.Resolver = f.Resolver
|
||||
if c.Resolver == "" {
|
||||
c.Resolver = health.ResolverSelf
|
||||
c.Resolver = cc.ResolverSelf
|
||||
}
|
||||
}
|
||||
if len(f.Evidence) > 0 {
|
||||
c.Evidence = append([]health.Evidence(nil), f.Evidence...)
|
||||
c.Evidence = append([]seat.Evidence(nil), f.Evidence...)
|
||||
c.LastObserved = f.Evidence[0].At
|
||||
} else if c.Evidence == nil {
|
||||
c.Evidence = []health.Evidence{{At: f.At, Said: f.Summary}}
|
||||
c.Evidence = []seat.Evidence{{At: f.At, Said: f.Summary}}
|
||||
c.LastObserved = f.At
|
||||
}
|
||||
c.Observations = len(c.Evidence)
|
||||
if c.Actions == nil {
|
||||
c.Actions = []health.Action{}
|
||||
c.Actions = []seat.Action{}
|
||||
}
|
||||
_ = now
|
||||
}
|
||||
|
||||
// overlay writes the healers' attempts into the condition (clause 7); nothing of the controller's bucket (issue 1126).
|
||||
func (k *Keeper) overlay(c *health.Condition, now time.Time) {
|
||||
func (k *Keeper) overlay(c *Condition, now time.Time) {
|
||||
k.mu.Lock()
|
||||
var mine []health.Attempt
|
||||
var mine []meshhealers.AttemptsByAttemptValue
|
||||
for _, a := range k.attempts {
|
||||
if a.Condition == c.Key {
|
||||
mine = append(mine, a)
|
||||
@@ -429,25 +440,25 @@ func (k *Keeper) overlay(c *health.Condition, now time.Time) {
|
||||
k.mu.Unlock()
|
||||
sort.Slice(mine, func(i, j int) bool { return mine[i].Begun.Before(mine[j].Begun) })
|
||||
for _, a := range mine {
|
||||
t := a.Tried()
|
||||
t := triedOf(a)
|
||||
if !containsTried(c.Tried, t) {
|
||||
c.Tried = append(c.Tried, t)
|
||||
}
|
||||
switch a.Outcome {
|
||||
case health.OutcomeEscalated:
|
||||
c.Resolver, c.Severity = health.ResolverOperator, health.Urgent
|
||||
case meshhealers.AttemptsByAttemptValueOutcomeEscalated:
|
||||
c.Resolver, c.Severity = cc.ResolverOperator, cc.Urgent
|
||||
default:
|
||||
if c.Resolver != health.ResolverOperator {
|
||||
c.Resolver = health.ResolverHealer(a.Healer)
|
||||
if c.Resolver != cc.ResolverOperator {
|
||||
c.Resolver = cc.ResolverHealer(a.Healer)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(c.Tried) > health.KeptTried {
|
||||
c.Tried = c.Tried[len(c.Tried)-health.KeptTried:]
|
||||
if len(c.Tried) > cc.KeptTried {
|
||||
c.Tried = c.Tried[len(c.Tried)-cc.KeptTried:]
|
||||
}
|
||||
}
|
||||
|
||||
func containsTried(list []health.Tried, t health.Tried) bool {
|
||||
func containsTried(list []Tried, t Tried) bool {
|
||||
for _, x := range list {
|
||||
if x.At.Equal(t.At) && x.By == t.By && x.What == t.What {
|
||||
return true
|
||||
@@ -458,47 +469,47 @@ func containsTried(list []health.Tried, t health.Tried) bool {
|
||||
|
||||
// transitions are the events between what was open and what is now, in the controller's order: raised or reopened;
|
||||
// else severity, resolver, silence and words changes. An observation that changes none of these says nothing.
|
||||
func transitions(was, now health.Condition, open bool) []health.Event {
|
||||
func transitions(was, now Condition, open bool) []Event {
|
||||
if !open {
|
||||
change := health.ChangeRaised
|
||||
change := cc.ChangeRaised
|
||||
if now.Count > 1 {
|
||||
change = health.ChangeReopened
|
||||
change = cc.ChangeReopened
|
||||
}
|
||||
return []health.Event{{Change: change}}
|
||||
return []Event{{Change: change}}
|
||||
}
|
||||
var out []health.Event
|
||||
var out []Event
|
||||
if was.Severity != now.Severity {
|
||||
out = append(out, health.Event{Change: health.ChangeSeverity, Was: string(was.Severity)})
|
||||
out = append(out, Event{Change: cc.ChangeSeverity, Was: string(was.Severity)})
|
||||
}
|
||||
if was.Resolver != now.Resolver {
|
||||
out = append(out, health.Event{Change: health.ChangeResolver, Was: was.Resolver})
|
||||
out = append(out, Event{Change: cc.ChangeResolver, Was: was.Resolver})
|
||||
}
|
||||
switch {
|
||||
case was.Silenced == nil && now.Silenced != nil:
|
||||
out = append(out, health.Event{Change: health.ChangeSilenced, Why: now.Silenced.Why})
|
||||
out = append(out, Event{Change: cc.ChangeSilenced, Why: now.Silenced.Why})
|
||||
case was.Silenced != nil && now.Silenced == nil:
|
||||
out = append(out, health.Event{Change: health.ChangeUnsilenced})
|
||||
out = append(out, Event{Change: cc.ChangeUnsilenced})
|
||||
}
|
||||
if was.Needs != now.Needs || !sameActions(was.Actions, now.Actions) {
|
||||
out = append(out, health.Event{Change: health.ChangeWords, Was: was.Needs})
|
||||
out = append(out, Event{Change: cc.ChangeWords, Was: was.Needs})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sameActions(a, b []health.Action) bool {
|
||||
func sameActions(a, b []seat.Action) bool {
|
||||
x, _ := json.Marshal(a)
|
||||
y, _ := json.Marshal(b)
|
||||
return string(x) == string(y) || (len(a) == 0 && len(b) == 0)
|
||||
}
|
||||
|
||||
func sameBody(a, b health.Condition) bool {
|
||||
func sameBody(a, b Condition) bool {
|
||||
x, _ := json.Marshal(a)
|
||||
y, _ := json.Marshal(b)
|
||||
return string(x) == string(y)
|
||||
}
|
||||
|
||||
// clear removes an open condition whose finding went, and says it (clause 2).
|
||||
func (k *Keeper) clear(key string, was health.Condition, rev uint64, now time.Time, why string) error {
|
||||
func (k *Keeper) clear(key string, was Condition, rev uint64, now time.Time, why string) error {
|
||||
// A delete at the revision read: a write that moved it meanwhile is read again.
|
||||
if _, err := k.Store.Update(ConditionsState, key, json.RawMessage("null"), rev); err != nil {
|
||||
return err
|
||||
@@ -510,19 +521,19 @@ func (k *Keeper) clear(key string, was health.Condition, rev uint64, now time.Ti
|
||||
k.cleared[key] = clearedMemo{at: now, count: was.Count, began: was.Began(), gaps: was.Gaps, silenced: was.Silenced, tried: was.Tried}
|
||||
k.mu.Unlock()
|
||||
cleared := now
|
||||
k.tell(health.Event{Condition: was, Event: health.EventCleared, At: now, Change: health.ChangeCleared, Why: why,
|
||||
Cleared: &cleared, Show: health.ShowKeeper(was.Key)})
|
||||
k.tell(Event{Condition: was, Event: seat.EventConditionCleared, At: now, Change: cc.ChangeCleared, Why: why,
|
||||
Cleared: &cleared, Show: cc.ShowKeeper(was.Key)})
|
||||
return nil
|
||||
}
|
||||
|
||||
// tell keeps the transition in history and says it on the seat. A transition not said is said in the log: the condition
|
||||
// and its history still hold it.
|
||||
func (k *Keeper) tell(e health.Event) {
|
||||
func (k *Keeper) tell(e Event) {
|
||||
k.mu.Lock()
|
||||
k.seq++
|
||||
hk := fmt.Sprintf("%020d-%06d", e.At.UnixNano(), k.seq)
|
||||
k.mu.Unlock()
|
||||
if _, err := k.Store.Create(HistoryState, hk, e); err != nil {
|
||||
if _, err := k.Store.Create(HistoryState, hk, e.Said()); err != nil {
|
||||
k.Logf("the %s of %s could not be kept in history: %v", e.Change, e.Key, err)
|
||||
}
|
||||
if k.Say == nil {
|
||||
@@ -534,12 +545,12 @@ func (k *Keeper) tell(e health.Event) {
|
||||
}
|
||||
|
||||
// Open is every open condition, ordered as `conditions` says them.
|
||||
func (k *Keeper) Open() ([]health.Condition, error) {
|
||||
func (k *Keeper) Open() ([]Condition, error) {
|
||||
keys, err := k.Store.Keys(ConditionsState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []health.Condition
|
||||
var out []Condition
|
||||
for _, key := range keys {
|
||||
raw, _, found, err := k.Store.Get(ConditionsState, key)
|
||||
if err != nil {
|
||||
@@ -548,13 +559,13 @@ func (k *Keeper) Open() ([]health.Condition, error) {
|
||||
if !found || string(raw) == "null" {
|
||||
continue
|
||||
}
|
||||
var c health.Condition
|
||||
if err := json.Unmarshal(raw, &c); err != nil {
|
||||
c, err := decodeCondition(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
health.Order(out)
|
||||
order(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -588,35 +599,35 @@ func (k *Keeper) Resync() (int, error) {
|
||||
|
||||
// Silence stops a condition's messages for a while, a hand act with why (clause 3, Limits 1): kept as its transition in
|
||||
// history, naming who and why.
|
||||
func (k *Keeper) Silence(key string, d time.Duration, by, why string) (health.Condition, error) {
|
||||
func (k *Keeper) Silence(key string, d time.Duration, by, why string) (Condition, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return health.Condition{}, errors.New("a silence says why")
|
||||
return Condition{}, errors.New("a silence says why")
|
||||
}
|
||||
if d <= 0 || d > health.MaxSilence {
|
||||
return health.Condition{}, fmt.Errorf("a silence lasts more than nothing and at most %s, not %s", health.MaxSilence, d)
|
||||
if d <= 0 || d > cc.MaxSilence {
|
||||
return Condition{}, fmt.Errorf("a silence lasts more than nothing and at most %s, not %s", cc.MaxSilence, d)
|
||||
}
|
||||
for i := 0; i < ConditionWrites; i++ {
|
||||
raw, rev, found, err := k.Store.Get(ConditionsState, key)
|
||||
if err != nil {
|
||||
return health.Condition{}, err
|
||||
return Condition{}, err
|
||||
}
|
||||
if !found || string(raw) == "null" {
|
||||
return health.Condition{}, fmt.Errorf("no condition %s is open", key)
|
||||
return Condition{}, fmt.Errorf("no condition %s is open", key)
|
||||
}
|
||||
var c health.Condition
|
||||
if err := json.Unmarshal(raw, &c); err != nil {
|
||||
return health.Condition{}, err
|
||||
c, err := decodeCondition(raw)
|
||||
if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
now := k.Now().UTC()
|
||||
c.Silenced = &health.Silence{Until: now.Add(d), By: by, Why: why, Since: now}
|
||||
if _, err := k.Store.Update(ConditionsState, key, c, rev); errors.Is(err, stdio.ErrChanged) {
|
||||
c.Silenced = &Silence{Until: now.Add(d), By: by, Why: why, Since: now}
|
||||
if _, err := k.Store.Update(ConditionsState, key, c.Stored(), rev); errors.Is(err, stdio.ErrChanged) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return health.Condition{}, err
|
||||
return Condition{}, err
|
||||
}
|
||||
k.tell(health.Event{Condition: c, Event: health.EventChanged, At: now, Change: health.ChangeSilenced,
|
||||
Why: why + " (" + by + ")", Show: health.ShowKeeper(key)})
|
||||
k.tell(Event{Condition: c, Event: seat.EventConditionChanged, At: now, Change: cc.ChangeSilenced,
|
||||
Why: why + " (" + by + ")", Show: cc.ShowKeeper(key)})
|
||||
return c, nil
|
||||
}
|
||||
return health.Condition{}, fmt.Errorf("the condition %s kept moving under the silence", key)
|
||||
return Condition{}, fmt.Errorf("the condition %s kept moving under the silence", key)
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// fakeStore is the bus's states in memory, with revisions, as the runtime answers them.
|
||||
@@ -102,12 +104,12 @@ type rig struct {
|
||||
k *Keeper
|
||||
st *fakeStore
|
||||
now time.Time
|
||||
said []health.Event
|
||||
said []Event
|
||||
}
|
||||
|
||||
func newRig() *rig {
|
||||
r := &rig{st: newFakeStore(), now: time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)}
|
||||
r.k = &Keeper{Store: r.st, Now: func() time.Time { return r.now }, Say: func(e health.Event) error {
|
||||
r.k = &Keeper{Store: r.st, Now: func() time.Time { return r.now }, Say: func(e Event) error {
|
||||
r.said = append(r.said, e)
|
||||
return nil
|
||||
}}
|
||||
@@ -123,22 +125,39 @@ func (r *rig) changes() string {
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
func (r *rig) open(key string) (health.Condition, bool) {
|
||||
func (r *rig) open(key string) (Condition, bool) {
|
||||
raw, _, ok, _ := r.st.Get(ConditionsState, key)
|
||||
var c health.Condition
|
||||
var c Condition
|
||||
if ok {
|
||||
_ = json.Unmarshal(raw, &c)
|
||||
// The seat's layout, read strictly: a field the seat does not name is a fault of the keeper's write.
|
||||
v, err := seat.DecodeConditionsByKeyValue(raw)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("the condition %s is not the seat's layout: %v: %s", key, err, raw))
|
||||
}
|
||||
c = conditionOf(v)
|
||||
}
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// finding is an observer's finding as mesh-sdk's layout writes it (health.EncodeFinding).
|
||||
func finding(t *testing.T, observer, scope, id, kind, source string, sev health.Severity, at time.Time) (string, []byte) {
|
||||
// encodeFinding writes a finding as an observer does: the seat's layout, checked.
|
||||
func encodeFinding(f seat.FindingsByKeyValue) ([]byte, error) {
|
||||
f.V = cc.FindingVersion
|
||||
if f.Evidence == nil {
|
||||
f.Evidence = []seat.Evidence{}
|
||||
}
|
||||
if err := cc.CheckFinding(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(f)
|
||||
}
|
||||
|
||||
// finding is an observer's finding as mesh-sdk's layout writes it (encodeFinding).
|
||||
func finding(t *testing.T, observer, scope, id, kind, source string, sev string, at time.Time) (string, []byte) {
|
||||
t.Helper()
|
||||
f := health.Finding{Key: health.Key(scope, id, kind), Kind: kind, Subject: health.Subject{Scope: scope, ID: id},
|
||||
f := seat.FindingsByKeyValue{Key: cc.Key(scope, id, kind), Kind: kind, Subject: seat.Subject{Scope: scope, ID: id},
|
||||
Severity: sev, Summary: kind + " of " + id, Headline: kind, Explanation: "It is " + kind + ".", Source: source,
|
||||
Observer: observer, At: at, Evidence: []health.Evidence{{At: at, Said: kind}}}
|
||||
raw, err := health.EncodeFinding(f)
|
||||
Observer: observer, At: at, Evidence: []seat.Evidence{{At: at, Said: kind}}}
|
||||
raw, err := encodeFinding(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -150,43 +169,43 @@ func finding(t *testing.T, observer, scope, id, kind, source string, sev health.
|
||||
// minutes is the same fault; a silence is a transition in history.
|
||||
func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
r := newRig()
|
||||
key, raw := finding(t, watchman, health.ScopeMachine, "g14", "silent", "S1", health.Warning, r.now)
|
||||
key, raw := finding(t, watchman, cc.ScopeMachine, "g14", "silent", "S1", cc.Warning, r.now)
|
||||
steps := []struct {
|
||||
name string
|
||||
do func() error
|
||||
said string
|
||||
check func(c health.Condition, open bool) string
|
||||
check func(c Condition, open bool) string
|
||||
}{
|
||||
{name: "a finding written raises", do: func() error { return r.k.Finding(watchman, key, raw, false) },
|
||||
said: "raised machine.g14.silent", check: func(c health.Condition, open bool) string {
|
||||
if !open || c.Severity != health.Warning || c.Resolver != health.ResolverSelf || c.Epoch != 0 || c.Count != 1 {
|
||||
said: "raised machine.g14.silent", check: func(c Condition, open bool) string {
|
||||
if !open || c.Severity != cc.Warning || c.Resolver != cc.ResolverSelf || c.Epoch != 0 || c.Count != 1 {
|
||||
return fmt.Sprintf("%+v", c)
|
||||
}
|
||||
return ""
|
||||
}},
|
||||
{name: "the same finding again says nothing", do: func() error { return r.k.Finding(watchman, key, raw, false) }},
|
||||
{name: "an observer gone blind clears nothing", do: func() error { r.k.Blind(watchman, "refused"); _, err := r.k.Resync(); return err },
|
||||
check: func(c health.Condition, open bool) string {
|
||||
check: func(c Condition, open bool) string {
|
||||
if !open {
|
||||
return "cleared by blindness"
|
||||
}
|
||||
return ""
|
||||
}},
|
||||
{name: "a healer's attempt reaches tried and the resolver", do: func() error {
|
||||
a, _ := json.Marshal(health.Attempt{Healer: "H3", Condition: key, Act: "asserted", Outcome: health.OutcomeActed, Begun: r.now})
|
||||
a, _ := json.Marshal(meshhealers.AttemptsByAttemptValue{Healer: "H3", Condition: key, Act: "asserted", Outcome: meshhealers.AttemptsByAttemptValueOutcomeActed, Begun: r.now})
|
||||
return r.k.Attempt("h3-1", a, false)
|
||||
}, said: "resolver machine.g14.silent", check: func(c health.Condition, _ bool) string {
|
||||
}, said: "resolver machine.g14.silent", check: func(c Condition, _ bool) string {
|
||||
if c.Resolver != "healer:H3" || len(c.Tried) != 1 || c.Tried[0].By != "healer H3" {
|
||||
return fmt.Sprintf("%+v", c)
|
||||
}
|
||||
return ""
|
||||
}},
|
||||
{name: "an escalation makes it urgent, the operator's", do: func() error {
|
||||
a, _ := json.Marshal(health.Attempt{Healer: "H3", Condition: key, Act: "handed over", Outcome: health.OutcomeEscalated,
|
||||
a, _ := json.Marshal(meshhealers.AttemptsByAttemptValue{Healer: "H3", Condition: key, Act: "handed over", Outcome: meshhealers.AttemptsByAttemptValueOutcomeEscalated,
|
||||
Begun: r.now.Add(time.Minute)})
|
||||
return r.k.Attempt("h3-2", a, false)
|
||||
}, said: "severity machine.g14.silent, resolver machine.g14.silent", check: func(c health.Condition, _ bool) string {
|
||||
if c.Severity != health.Urgent || c.Resolver != health.ResolverOperator || len(c.Tried) != 2 {
|
||||
}, said: "severity machine.g14.silent, resolver machine.g14.silent", check: func(c Condition, _ bool) string {
|
||||
if c.Severity != cc.Urgent || c.Resolver != cc.ResolverOperator || len(c.Tried) != 2 {
|
||||
return fmt.Sprintf("%+v", c)
|
||||
}
|
||||
return ""
|
||||
@@ -196,7 +215,7 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
return err
|
||||
}, said: "silenced machine.g14.silent"},
|
||||
{name: "the finding deleted clears", do: func() error { return r.k.Finding(watchman, key, nil, true) },
|
||||
said: "cleared machine.g14.silent", check: func(_ health.Condition, open bool) string {
|
||||
said: "cleared machine.g14.silent", check: func(_ Condition, open bool) string {
|
||||
if open {
|
||||
return "still open"
|
||||
}
|
||||
@@ -205,7 +224,7 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
{name: "raised again within ten minutes is a reopening", do: func() error {
|
||||
r.now = r.now.Add(5 * time.Minute)
|
||||
return r.k.Finding(watchman, key, raw, false)
|
||||
}, said: "reopened machine.g14.silent", check: func(c health.Condition, _ bool) string {
|
||||
}, said: "reopened machine.g14.silent", check: func(c Condition, _ bool) string {
|
||||
if c.Count != 2 || len(c.Gaps) != 1 {
|
||||
return fmt.Sprintf("%+v", c)
|
||||
}
|
||||
@@ -242,17 +261,17 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
// once by EndCarried, said as cleared; a condition an observer's finding holds stays; a second EndCarried clears nothing.
|
||||
func TestTheKeeperCarriesNoControllerConditionAndClearsWhatItCarriedOnce(t *testing.T) {
|
||||
r := newRig()
|
||||
key, raw := finding(t, watchman, health.ScopeMachine, "g14", "silent", "S1", health.Warning, r.now)
|
||||
key, raw := finding(t, watchman, cc.ScopeMachine, "g14", "silent", "S1", cc.Warning, r.now)
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.said = nil
|
||||
// Carried while the controller ran: two of kinds that ended with it, still open in conditions.
|
||||
for _, c := range []health.Condition{
|
||||
{Key: "core.controller.deaf", Kind: "controller-deaf", Severity: health.Urgent, Resolver: "self", Count: 1},
|
||||
{Key: "seat.vault.ace.holder-silent", Kind: "holder-silent", Severity: health.Warning, Resolver: "self", Count: 1},
|
||||
for _, c := range []Condition{
|
||||
{Key: "core.controller.deaf", Kind: "controller-deaf", Severity: cc.Urgent, Resolver: "self", Count: 1},
|
||||
{Key: "seat.vault.ace.holder-silent", Kind: "holder-silent", Severity: cc.Warning, Resolver: "self", Count: 1},
|
||||
} {
|
||||
if _, err := r.st.Create(ConditionsState, c.Key, c); err != nil {
|
||||
if _, err := r.st.Create(ConditionsState, c.Key, c.Stored()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -368,3 +387,34 @@ func viewFindingWriters(t *testing.T, modules string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ADR 0634 §1, §4 (issue 1966): the keeper reads a condition written before the seat named its layout (the controller's
|
||||
// body: `silenced` null, `actions` empty, the times with an offset) and writes it back in the seat's layout.
|
||||
func TestTheKeeperReadsTheOldLayoutAndWritesTheSeatsLayout(t *testing.T) {
|
||||
r := newRig()
|
||||
key, raw := finding(t, watchman, cc.ScopeMachine, "g14", "silent", "S1", cc.Warning, r.now)
|
||||
old := `{"key":"` + key + `","kind":"silent","subject":{"scope":"machine","id":"g14"},"severity":"warning",` +
|
||||
`"summary":"old","headline":"","explanation":"","resolved":"","needs":"","actions":[],"evidence":[],"source":"S1",` +
|
||||
`"raised":"2026-10-29T13:00:00+01:00","last-observed":"2026-10-29T13:00:00+01:00","observations":1,"count":1,` +
|
||||
`"resolver":"self","silenced":null,"epoch":0}`
|
||||
if _, err := r.st.Create(ConditionsState, key, json.RawMessage(old)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, ok := r.open(key)
|
||||
if !ok || c.Summary != "silent of g14" || !c.Raised.Equal(r.now) || c.Count != 1 {
|
||||
t.Fatalf("the old condition, settled: %+v (open %v)", c, ok)
|
||||
}
|
||||
if _, err := r.k.Silence(key, time.Hour, "tester", "known"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, _ := r.open(key); c.Silenced == nil || !c.Silenced.Until.Equal(r.now.Add(time.Hour)) {
|
||||
t.Errorf("the silence is not kept in the seat's layout: %+v", c.Silenced)
|
||||
}
|
||||
last := r.said[len(r.said)-1].Said()
|
||||
if last.Change != cc.ChangeSilenced || last.Silenced == nil || last.Subject == nil || last.Subject.ID != "g14" || last.Show == "" {
|
||||
t.Errorf("the event the seat hears: %+v", last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -55,8 +55,8 @@ func (runtimeStore) CreateTTL(state, key string, value any, ttl time.Duration) e
|
||||
}
|
||||
|
||||
// sayOnSeat says a transition on the seat, as the controller says it on its own (ADR 0564 clause 3).
|
||||
func sayOnSeat(e health.Event) error {
|
||||
return stdio.SeatPublish("mesh.seat."+Seat+".event."+e.Event, e, "")
|
||||
func sayOnSeat(e Event) error {
|
||||
return stdio.SeatPublish("mesh.seat."+Seat+".event."+e.Event, e.Said(), "")
|
||||
}
|
||||
|
||||
// NewKeeper is the keeper over the runtime.
|
||||
@@ -120,11 +120,11 @@ func Keep(k *Keeper, v *views.Reader) {
|
||||
}
|
||||
watch(k, v, WantsRead, "", func(c stdio.StateChange) error { return said(k.Want(c.Key, c.Value, c.Op != "put")) })
|
||||
watch(k, v, HavesRead, "", func(c stdio.StateChange) error { return said(k.Have(c.Key, c.Value, c.Op != "put")) })
|
||||
watch(k, v, AttemptsRead, health.HealersModule+" attempts", func(c stdio.StateChange) error {
|
||||
watch(k, v, AttemptsRead, healersModule+" attempts", func(c stdio.StateChange) error {
|
||||
return said(k.Attempt(c.Key, c.Value, c.Op != "put"))
|
||||
})
|
||||
for _, o := range Observers {
|
||||
watch(k, v, o+"."+health.FindingsState, o, func(c stdio.StateChange) error {
|
||||
watch(k, v, o+"."+seat.StateFindings, o, func(c stdio.StateChange) error {
|
||||
return said(k.Finding(o, c.Key, c.Value, c.Op != "put"))
|
||||
})
|
||||
}
|
||||
@@ -155,8 +155,8 @@ func NewViews(k *Keeper) *views.Reader {
|
||||
v := &views.Reader{Module: Module, Logf: logf, Findings: ownFindings{k}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
||||
v.Follow(delivery.AssignerModule, "wants", WantsTool)
|
||||
v.Follow(delivery.AssignerModule, "placement", PlacementTool)
|
||||
v.Follow(delivery.AssignerModule, health.FindingsState, FindingsTool)
|
||||
v.Follow(delivery.IndexerModule, health.FindingsState, IndexerFindingsTool)
|
||||
v.Follow(delivery.AssignerModule, seat.StateFindings, FindingsTool)
|
||||
v.Follow(delivery.IndexerModule, seat.StateFindings, IndexerFindingsTool)
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
|
||||
)
|
||||
|
||||
@@ -27,17 +28,17 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
t.Fatalf("the manifest does not decode: %v", err)
|
||||
}
|
||||
catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions()))
|
||||
seat, _ := catalogue.SeatNamed(Seat)
|
||||
def, _ := catalogue.SeatNamed(Seat)
|
||||
if m.Module != Module || m.RunsAs != Module || len(m.Claims) != 1 || m.Claims[0].Name != Seat ||
|
||||
!slices.Equal(Verbs, catalogue.VerbNames(seat.Serves)) {
|
||||
t.Errorf("module %s runs as %s and claims %+v; the seat serves %v", m.Module, m.RunsAs, m.Claims, catalogue.VerbNames(seat.Serves))
|
||||
!slices.Equal(Verbs, catalogue.VerbNames(def.Serves)) {
|
||||
t.Errorf("module %s runs as %s and claims %+v; the def serves %v", m.Module, m.RunsAs, m.Claims, catalogue.VerbNames(def.Serves))
|
||||
}
|
||||
// The assigner's wants, placement and findings, and the indexer's findings, are asked once of their read tools and
|
||||
// followed by their events, not read (ADR 0615 §1).
|
||||
reads := []string{HavesRead, AttemptsRead}
|
||||
for _, o := range Observers {
|
||||
if o != delivery.AssignerModule && o != delivery.IndexerModule {
|
||||
reads = append(reads, o+"."+health.FindingsState)
|
||||
reads = append(reads, o+"."+seat.StateFindings)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(m.Invokes, []string{WantsTool, PlacementTool, FindingsTool, IndexerFindingsTool, "seat:mesh-hand-acts.hand-act"}) ||
|
||||
@@ -48,7 +49,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
slices.Sort(reads)
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if slices.Contains(m.Reads, "mesh-controller.conditions") || slices.Contains(m.Reads, "mesh-controller.lease") || !slices.Equal(got, reads) || slices.Contains(m.Reads, health.HealersModule+"."+health.WouldState) {
|
||||
if slices.Contains(m.Reads, "mesh-controller.conditions") || slices.Contains(m.Reads, "mesh-controller.lease") || !slices.Equal(got, reads) || slices.Contains(m.Reads, healersModule+"."+meshhealers.StateWould) {
|
||||
t.Errorf("reads %v, want %v and never the healers' would", got, reads)
|
||||
}
|
||||
ttl, keyTTL := map[string]int{}, map[string]bool{}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
// The keeper's working model of a condition and its events, over the seat mesh-conditions' contract types (novox/hq
|
||||
// ADR 0564 clause 3, ADR 0634 §1, §4). The seat's types keep an optional time as a pointer; the keeper's rules read
|
||||
// better over plain times, so the keeper works on its own Condition and Event and converts at the edge: what it writes
|
||||
// to `conditions` and `history`, and what it says on the seat, is the seat's form, and what it reads is converted back.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
)
|
||||
|
||||
// Silence is a person saying they know: no messages until it ends. The condition stays open.
|
||||
type Silence struct {
|
||||
Until time.Time
|
||||
By string
|
||||
Why string
|
||||
Since time.Time
|
||||
}
|
||||
|
||||
// Gap is a stretch in which a fault was cleared, between its clearing and its reopening.
|
||||
type Gap struct{ Cleared, Reopened time.Time }
|
||||
|
||||
// Tried is one healer's try at a condition, as the condition carries it in `tried`.
|
||||
type Tried struct {
|
||||
At time.Time
|
||||
What string
|
||||
Outcome string
|
||||
By string
|
||||
}
|
||||
|
||||
// Condition is one open condition as the keeper works on it.
|
||||
type Condition struct {
|
||||
Key string
|
||||
Kind string
|
||||
Subject seat.Subject
|
||||
Severity string
|
||||
Summary string
|
||||
Headline string
|
||||
Explanation string
|
||||
Resolved string
|
||||
Needs string
|
||||
Actions []seat.Action
|
||||
Evidence []seat.Evidence
|
||||
Source string
|
||||
Raised time.Time
|
||||
LastObserved time.Time
|
||||
// First and Gaps are the fault's earlier life: a reopening within ReopenWithin is the same fault.
|
||||
First time.Time
|
||||
Gaps []Gap
|
||||
Observations int
|
||||
Count int
|
||||
Tried []Tried
|
||||
Resolver string
|
||||
// Silenced is nil when no silence is in force.
|
||||
Silenced *Silence
|
||||
Epoch int64
|
||||
LooksUnread string
|
||||
}
|
||||
|
||||
// Escalated says a healer tried and its budget is spent: the operator resolves it now.
|
||||
func (c Condition) Escalated() bool { return c.Resolver == cc.ResolverOperator && len(c.Tried) > 0 }
|
||||
|
||||
// Began is when this fault began as the mesh knows it: its first raising, a reopening within ReopenWithin being the
|
||||
// same fault again.
|
||||
func (c Condition) Began() time.Time {
|
||||
if !c.First.IsZero() && c.First.Before(c.Raised) {
|
||||
return c.First
|
||||
}
|
||||
return c.Raised
|
||||
}
|
||||
|
||||
// Event is one transition: the condition itself with what happened to it beside.
|
||||
type Event struct {
|
||||
Condition
|
||||
Event string
|
||||
At time.Time
|
||||
Change string
|
||||
Was string
|
||||
Why string
|
||||
Cleared *time.Time
|
||||
Show string
|
||||
}
|
||||
|
||||
func timeP(t time.Time) *time.Time {
|
||||
if t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &t
|
||||
}
|
||||
|
||||
func timeOf(t *time.Time) time.Time {
|
||||
if t == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return *t
|
||||
}
|
||||
|
||||
// Stored is the condition as the seat's state `conditions` keeps it.
|
||||
func (c Condition) Stored() seat.ConditionsByKeyValue {
|
||||
v := seat.ConditionsByKeyValue{Key: c.Key, Kind: c.Kind, Subject: c.Subject, Severity: c.Severity, Summary: c.Summary,
|
||||
Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: c.Actions,
|
||||
Evidence: c.Evidence, Source: c.Source, Raised: c.Raised, LastObserved: c.LastObserved, First: timeP(c.First),
|
||||
Observations: int64(c.Observations), Count: int64(c.Count), Resolver: c.Resolver, Epoch: c.Epoch,
|
||||
LooksUnread: c.LooksUnread}
|
||||
for _, g := range c.Gaps {
|
||||
v.Gaps = append(v.Gaps, seat.ConditionsByKeyValueGapsItem{Cleared: timeP(g.Cleared), Reopened: timeP(g.Reopened)})
|
||||
}
|
||||
for _, t := range c.Tried {
|
||||
v.Tried = append(v.Tried, seat.ConditionsByKeyValueTriedItem{At: timeP(t.At), What: t.What, Outcome: t.Outcome, By: t.By})
|
||||
}
|
||||
if c.Silenced != nil {
|
||||
v.Silenced = &seat.ConditionsByKeyValueSilenced{Until: timeP(c.Silenced.Until), By: c.Silenced.By,
|
||||
Why: c.Silenced.Why, Since: timeP(c.Silenced.Since)}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// conditionOf is the working condition of a stored one.
|
||||
func conditionOf(v seat.ConditionsByKeyValue) Condition {
|
||||
c := Condition{Key: v.Key, Kind: v.Kind, Subject: v.Subject, Severity: v.Severity, Summary: v.Summary,
|
||||
Headline: v.Headline, Explanation: v.Explanation, Resolved: v.Resolved, Needs: v.Needs, Actions: v.Actions,
|
||||
Evidence: v.Evidence, Source: v.Source, Raised: v.Raised, LastObserved: v.LastObserved, First: timeOf(v.First),
|
||||
Observations: int(v.Observations), Count: int(v.Count), Resolver: v.Resolver, Epoch: v.Epoch,
|
||||
LooksUnread: v.LooksUnread}
|
||||
for _, g := range v.Gaps {
|
||||
c.Gaps = append(c.Gaps, Gap{Cleared: timeOf(g.Cleared), Reopened: timeOf(g.Reopened)})
|
||||
}
|
||||
for _, t := range v.Tried {
|
||||
c.Tried = append(c.Tried, Tried{At: timeOf(t.At), What: t.What, Outcome: t.Outcome, By: t.By})
|
||||
}
|
||||
if v.Silenced != nil {
|
||||
c.Silenced = &Silence{Until: timeOf(v.Silenced.Until), By: v.Silenced.By, Why: v.Silenced.Why,
|
||||
Since: timeOf(v.Silenced.Since)}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// decodeCondition reads a condition of the state `conditions`; the seat's layout is read as it is written, one field
|
||||
// the keeper does not know being the seat's change, not a fault.
|
||||
func decodeCondition(raw []byte) (Condition, error) {
|
||||
var v seat.ConditionsByKeyValue
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
return conditionOf(v), nil
|
||||
}
|
||||
|
||||
// Said is the event as the seat says it and `history` keeps it.
|
||||
func (e Event) Said() seat.ConditionEvent {
|
||||
s := e.Condition.Stored()
|
||||
v := seat.ConditionEvent{Key: s.Key, Kind: s.Kind, Subject: &s.Subject, Severity: s.Severity, Summary: s.Summary,
|
||||
Headline: s.Headline, Explanation: s.Explanation, Resolved: s.Resolved, Needs: s.Needs, Actions: s.Actions,
|
||||
Evidence: s.Evidence, Source: s.Source, Raised: timeP(s.Raised), LastObserved: timeP(s.LastObserved), First: s.First,
|
||||
Observations: s.Observations, Count: s.Count, Resolver: s.Resolver, Epoch: s.Epoch, LooksUnread: s.LooksUnread,
|
||||
Event: e.Event, At: e.At, Change: e.Change, Was: e.Was, Why: e.Why, Cleared: e.Cleared, Show: e.Show}
|
||||
for _, g := range s.Gaps {
|
||||
v.Gaps = append(v.Gaps, seat.ConditionEventGapsItem{Cleared: g.Cleared, Reopened: g.Reopened})
|
||||
}
|
||||
for _, t := range s.Tried {
|
||||
v.Tried = append(v.Tried, seat.ConditionEventTriedItem{At: t.At, What: t.What, Outcome: t.Outcome, By: t.By})
|
||||
}
|
||||
if s.Silenced != nil {
|
||||
v.Silenced = &seat.ConditionEventSilenced{Until: s.Silenced.Until, By: s.Silenced.By, Why: s.Silenced.Why,
|
||||
Since: s.Silenced.Since}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// eventOf is the working event of one the seat says.
|
||||
func eventOf(v seat.ConditionEvent) Event {
|
||||
s := seat.ConditionsByKeyValue{Key: v.Key, Kind: v.Kind, Severity: v.Severity, Summary: v.Summary,
|
||||
Headline: v.Headline, Explanation: v.Explanation, Resolved: v.Resolved, Needs: v.Needs, Actions: v.Actions,
|
||||
Evidence: v.Evidence, Source: v.Source, Raised: timeOf(v.Raised), LastObserved: timeOf(v.LastObserved),
|
||||
First: v.First, Observations: v.Observations, Count: v.Count, Resolver: v.Resolver, Epoch: v.Epoch,
|
||||
LooksUnread: v.LooksUnread}
|
||||
if v.Subject != nil {
|
||||
s.Subject = *v.Subject
|
||||
}
|
||||
for _, g := range v.Gaps {
|
||||
s.Gaps = append(s.Gaps, seat.ConditionsByKeyValueGapsItem{Cleared: g.Cleared, Reopened: g.Reopened})
|
||||
}
|
||||
for _, t := range v.Tried {
|
||||
s.Tried = append(s.Tried, seat.ConditionsByKeyValueTriedItem{At: t.At, What: t.What, Outcome: t.Outcome, By: t.By})
|
||||
}
|
||||
if v.Silenced != nil {
|
||||
s.Silenced = &seat.ConditionsByKeyValueSilenced{Until: v.Silenced.Until, By: v.Silenced.By, Why: v.Silenced.Why,
|
||||
Since: v.Silenced.Since}
|
||||
}
|
||||
return Event{Condition: conditionOf(s), Event: v.Event, At: v.At, Change: v.Change, Was: v.Was, Why: v.Why,
|
||||
Cleared: v.Cleared, Show: v.Show}
|
||||
}
|
||||
|
||||
// order sorts conditions as `conditions` says them.
|
||||
func order(list []Condition) {
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
if list[i].Severity != list[j].Severity {
|
||||
return list[i].Severity == cc.Urgent
|
||||
}
|
||||
if !list[i].Raised.Equal(list[j].Raised) {
|
||||
return list[i].Raised.Before(list[j].Raised)
|
||||
}
|
||||
return list[i].Key < list[j].Key
|
||||
})
|
||||
}
|
||||
|
||||
// decodeFinding reads an observer's finding of the seat's state `findings`, refusing one the keeper could not make a
|
||||
// condition of. A finding written before `ref` was named has it as `token`.
|
||||
func decodeFinding(data []byte) (seat.FindingsByKeyValue, error) {
|
||||
var f seat.FindingsByKeyValue
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return f, fmt.Errorf("a finding is not readable: %w", err)
|
||||
}
|
||||
if f.Ref == "" {
|
||||
var old struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
_ = json.Unmarshal(data, &old)
|
||||
f.Ref = old.Token
|
||||
}
|
||||
if f.V != cc.FindingVersion {
|
||||
return f, fmt.Errorf("a finding of version %d, and this reads %d", f.V, cc.FindingVersion)
|
||||
}
|
||||
return f, cc.CheckFinding(f)
|
||||
}
|
||||
|
||||
// triedOf is the attempt as the condition carries it in `tried`.
|
||||
func triedOf(a meshhealers.AttemptsByAttemptValue) Tried {
|
||||
outcome := a.Outcome
|
||||
if a.Said != "" {
|
||||
outcome += ": " + a.Said
|
||||
}
|
||||
return Tried{At: a.Begun, What: a.Act, Outcome: outcome, By: "healer " + a.Healer}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
)
|
||||
|
||||
// Verbs are the seat's verbs, as the manifest claims them.
|
||||
@@ -31,7 +33,7 @@ const briefEvidence = 1
|
||||
|
||||
// inBrief is a condition in a list: its newest evidence, with how many it keeps and how to read it whole.
|
||||
type inBrief struct {
|
||||
health.Condition
|
||||
seat.ConditionsByKeyValue
|
||||
EvidenceKept int `json:"evidence-kept,omitempty"`
|
||||
More string `json:"more,omitempty"`
|
||||
}
|
||||
@@ -67,7 +69,8 @@ func Tools(k *Keeper) []stdio.Tool {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k.Silence(strArg(args, "key"), d, stdio.CallerOf(args), strArg(args, "why"))
|
||||
c, err := k.Silence(strArg(args, "key"), d, stdio.CallerOf(args), strArg(args, "why"))
|
||||
return c.Stored(), err
|
||||
}},
|
||||
{Name: Seat + ".resync", Description: "Read every finding again and raise and clear what differs.",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
@@ -109,11 +112,11 @@ func (k *Keeper) ConditionsVerb(args map[string]any) (any, error) {
|
||||
if !found || string(raw) == "null" {
|
||||
return nil, fmt.Errorf("no condition %s is open", key)
|
||||
}
|
||||
var c health.Condition
|
||||
return c, json.Unmarshal(raw, &c)
|
||||
c, err := decodeCondition(raw)
|
||||
return c.Stored(), err
|
||||
}
|
||||
scope, severity, machine := strArg(args, "scope"), strArg(args, "severity"), strArg(args, "machine")
|
||||
if severity != "" && severity != string(health.Urgent) && severity != string(health.Warning) {
|
||||
if severity != "" && severity != cc.Urgent && severity != cc.Warning {
|
||||
return nil, fmt.Errorf("a severity is urgent or warning, not %q", severity)
|
||||
}
|
||||
open, err := k.Open()
|
||||
@@ -123,13 +126,13 @@ func (k *Keeper) ConditionsVerb(args map[string]any) (any, error) {
|
||||
list := []inBrief{}
|
||||
counted := map[string]int{}
|
||||
for _, c := range open {
|
||||
if (scope != "" && c.Subject.Scope != scope) || (severity != "" && string(c.Severity) != severity) ||
|
||||
if (scope != "" && c.Subject.Scope != scope) || (severity != "" && c.Severity != severity) ||
|
||||
(machine != "" && !concerns(c, machine)) {
|
||||
continue
|
||||
}
|
||||
b := inBrief{Condition: c}
|
||||
b := inBrief{ConditionsByKeyValue: c.Stored()}
|
||||
if len(c.Evidence) > briefEvidence {
|
||||
b.EvidenceKept, b.Evidence, b.More = len(c.Evidence), c.Evidence[:briefEvidence:briefEvidence], health.ShowKeeper(c.Key)
|
||||
b.EvidenceKept, b.Evidence, b.More = len(c.Evidence), c.Evidence[:briefEvidence:briefEvidence], cc.ShowKeeper(c.Key)
|
||||
}
|
||||
list = append(list, b)
|
||||
counted[c.Source+" "+c.Kind]++
|
||||
@@ -140,18 +143,18 @@ func (k *Keeper) ConditionsVerb(args map[string]any) (any, error) {
|
||||
}
|
||||
|
||||
// concerns says whether a condition is about a machine: it names it, or its key does.
|
||||
func concerns(c health.Condition, machine string) bool {
|
||||
func concerns(c Condition, machine string) bool {
|
||||
return c.Subject.Machine == machine || slices.Contains(c.Subject.Also, machine) ||
|
||||
slices.Contains(strings.Split(c.Key, "."), machine)
|
||||
}
|
||||
|
||||
// History is the newest transitions, at most limit, of one key or all.
|
||||
func (k *Keeper) History(key string, limit int) ([]health.Event, error) {
|
||||
func (k *Keeper) History(key string, limit int) ([]seat.ConditionEvent, error) {
|
||||
all, err := k.HistorySince(key, time.Time{}, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events := all["history"].([]health.Event)
|
||||
events := all["history"].([]seat.ConditionEvent)
|
||||
slices.Reverse(events)
|
||||
if len(events) > limit {
|
||||
events = events[:limit]
|
||||
@@ -165,13 +168,13 @@ func (k *Keeper) HistorySince(key string, since time.Time, days int) (map[string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events := []health.Event{}
|
||||
events := []seat.ConditionEvent{}
|
||||
for _, hk := range keys {
|
||||
raw, _, found, err := k.Store.Get(HistoryState, hk)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
var e health.Event
|
||||
var e seat.ConditionEvent
|
||||
if json.Unmarshal(raw, &e) != nil || e.At.Before(since) || (key != "" && e.Key != key) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
|
||||
@@ -296,15 +297,15 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) {
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
at := time.Now().UTC()
|
||||
f := health.Finding{Key: "machine.g14.silent", Kind: "silent", Subject: health.Subject{Scope: health.ScopeMachine, ID: "g14",
|
||||
Machine: "g14"}, Severity: health.Warning, Summary: "g14 is silent", Headline: "g14 is offline",
|
||||
f := seat.FindingsByKeyValue{Key: "machine.g14.silent", Kind: "silent", Subject: seat.Subject{Scope: cc.ScopeMachine, ID: "g14",
|
||||
Machine: "g14"}, Severity: cc.Warning, Summary: "g14 is silent", Headline: "g14 is offline",
|
||||
Explanation: "g14 said nothing for three minutes.", Resolved: "g14 is back", Source: "S1",
|
||||
Observer: health.WatchmanModule, At: at, Evidence: []health.Evidence{{At: at, Said: "no presence for 3m"}}}
|
||||
raw, err := health.EncodeFinding(f)
|
||||
Observer: watchmanModule, At: at, Evidence: []seat.Evidence{{At: at, Said: "no presence for 3m"}}}
|
||||
raw, err := encodeFinding(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
findings, _ := js.KeyValue(ctx, health.WatchmanModule+"_findings")
|
||||
findings, _ := js.KeyValue(ctx, watchmanModule+"_findings")
|
||||
if _, err := findings.Put(ctx, f.Key, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -312,9 +313,9 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("no condition-raised was said: %v", err)
|
||||
}
|
||||
var e health.Event
|
||||
var e Event
|
||||
if err := json.Unmarshal(msg.Data, &e); err != nil || msg.Subject != "mesh.seat.mesh-conditions.event.condition-raised" ||
|
||||
e.Key != f.Key || e.Change != health.ChangeRaised || e.Show != health.ShowKeeper(f.Key) || e.Headline != f.Headline {
|
||||
e.Key != f.Key || e.Change != cc.ChangeRaised || e.Show != cc.ShowKeeper(f.Key) || e.Headline != f.Headline {
|
||||
t.Fatalf("said %s on %s", msg.Data, msg.Subject)
|
||||
}
|
||||
listed, err := p.call(Seat+".conditions", map[string]any{})
|
||||
|
||||
@@ -5,6 +5,8 @@ go 1.26.0
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -39,8 +39,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// The events, by their local names under the keeper's seat.
|
||||
@@ -49,7 +49,7 @@ const (
|
||||
EventChanged = "condition-changed"
|
||||
EventCleared = "condition-cleared"
|
||||
// ConditionSeat is the role the events are stated under, and the verb `conditions` is asked of.
|
||||
ConditionSeat = health.KeeperModule
|
||||
ConditionSeat = meshconditions.Seat
|
||||
)
|
||||
|
||||
// Severities: two levels, no more (to-be 45 §2).
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.26.0
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
|
||||
Reference in New Issue
Block a user