666 lines
21 KiB
Go
666 lines
21 KiB
Go
package main
|
|
|
|
// The condition keeper (novox/hq ADR 0564 clauses 2, 3, 7, 8, 9 and 13; issue 1952 C2, C3, C7): the one writer of the
|
|
// mesh's open conditions. It watches every observer's `findings`, the healers' `attempts`, the controller's lease and,
|
|
// until the lease write, the controller's `conditions`, and the assigner's wants and the nodes' haves for the hold. 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
|
|
// but its finding going (ADR 0231 §14).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// The module, its seat, and the states it keeps.
|
|
const (
|
|
Module = health.KeeperModule
|
|
Seat = "mesh-conditions"
|
|
|
|
ConditionsState = health.ConditionsState
|
|
TakeoverState = health.TakeoverState
|
|
HistoryState = "history"
|
|
HeldState = "held"
|
|
BoundsState = "bounds"
|
|
// AskedState keeps the keeper's asks of the operator (go/asker).
|
|
AskedState = "asked"
|
|
)
|
|
|
|
// HistoryKeptFor is how long a transition is kept (clause 3).
|
|
const HistoryKeptFor = 90 * 24 * time.Hour
|
|
|
|
// Observers are the modules whose `findings` the keeper reads (clause 2; ADR 0565 §8 for the assigner's).
|
|
var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.HealersModule, delivery.AssignerModule}
|
|
|
|
// The other states it reads, as `<module>.<name>`.
|
|
const (
|
|
AttemptsRead = health.HealersModule + "." + health.AttemptsState
|
|
ControllerConditions = "mesh-controller.conditions"
|
|
ControllerLease = "mesh-controller.lease"
|
|
ControllerLeaseKey = "holder"
|
|
WantsRead = delivery.AssignerModule + ".wants"
|
|
// PlacementRead is the mesh-placement 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).
|
|
PlacementRead = delivery.PlacementRead
|
|
HavesRead = "node-engine.have"
|
|
ConditionWrites = 8
|
|
)
|
|
|
|
// Store is the bus as the keeper uses it: its own states, written by compare-and-set, and a key with its own time to
|
|
// live for a hold's bound.
|
|
type Store interface {
|
|
Get(state, key string) (value []byte, revision uint64, found bool, err error)
|
|
Create(state, key string, value any) (uint64, error)
|
|
Update(state, key string, value any, revision uint64) (uint64, error)
|
|
Delete(state, key string) error
|
|
Keys(state string) ([]string, error)
|
|
CreateTTL(state, key string, value any, ttl time.Duration) error
|
|
}
|
|
|
|
// held is a finding or a carried condition kept while its module is delivered (clause 13 (b)).
|
|
type held struct {
|
|
Key string `json:"key"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Since time.Time `json:"since"`
|
|
Version string `json:"version"`
|
|
Wanted json.RawMessage `json:"-"`
|
|
}
|
|
|
|
// clearedMemo is what a cleared condition leaves for ReopenWithin: the same fault again is a reopening.
|
|
type clearedMemo struct {
|
|
at time.Time
|
|
count int
|
|
began time.Time
|
|
gaps []health.Gap
|
|
silenced *health.Silence
|
|
tried []health.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
|
|
Now func() time.Time
|
|
Logf func(format string, a ...any)
|
|
// Homes says, by kind, where each controller kind lives after the take-over (homes.go); Homes when nil.
|
|
Homes map[string]Home
|
|
// Bound is a delivery's hold bound; delivery.DefaultDeliveryBound when zero (hold.go).
|
|
Bound time.Duration
|
|
|
|
mu sync.Mutex
|
|
findings map[string]map[string]health.Finding
|
|
blind map[string]string
|
|
attempts map[string]health.Attempt
|
|
ctrl map[string]health.Condition
|
|
leaseOut bool
|
|
takeover health.Takeover
|
|
wants map[string]delivery.Want
|
|
haves map[string]delivery.Have
|
|
// holders are, by `<node>.<seat>`, the module the placement says holds the seat there.
|
|
holders map[string]string
|
|
stuck map[string]string
|
|
held map[string]held
|
|
// passed are the deliveries whose bound passed, by node and module, at the fingerprint wanted then; outOfHold the
|
|
// line a key raised out of a hold says (hold.go).
|
|
passed map[string]string
|
|
outOfHold map[string]string
|
|
cleared map[string]clearedMemo
|
|
seq uint64
|
|
}
|
|
|
|
func (k *Keeper) init() {
|
|
if k.findings == nil {
|
|
k.findings, k.blind, k.attempts, k.ctrl = map[string]map[string]health.Finding{}, map[string]string{},
|
|
map[string]health.Attempt{}, map[string]health.Condition{}
|
|
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.takeover = health.Takeover{V: health.TakeoverVersion, Follow: health.FollowController}
|
|
k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{}
|
|
}
|
|
if k.Now == nil {
|
|
k.Now = time.Now
|
|
}
|
|
if k.Logf == nil {
|
|
k.Logf = func(string, ...any) {}
|
|
}
|
|
}
|
|
|
|
// Finding takes one change of an observer's finding: a key put raises or changes its condition, a key deleted clears
|
|
// it (clause 2). A value that is not a finding is the observer's fault, said and left as it was.
|
|
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{}
|
|
}
|
|
if deleted {
|
|
delete(k.findings[observer], key)
|
|
} else {
|
|
f, err := health.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)
|
|
return nil
|
|
}
|
|
if f.Key != key {
|
|
k.mu.Unlock()
|
|
k.Logf("%s's finding under %s names the key %s, so it is left as it stood", observer, key, f.Key)
|
|
return nil
|
|
}
|
|
k.findings[observer][key] = f
|
|
}
|
|
k.mu.Unlock()
|
|
return k.Settle(key)
|
|
}
|
|
|
|
// Blind says an observer's findings cannot be read: its keys stay as they were, and nothing is cleared for not seeing
|
|
// (ADR 0227 §4). Empty why says it is read again.
|
|
func (k *Keeper) Blind(observer, why string) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
k.init()
|
|
if why == "" {
|
|
delete(k.blind, observer)
|
|
} else {
|
|
k.blind[observer] = why
|
|
}
|
|
}
|
|
|
|
// Attempt takes one change of the healers' `attempts`: the condition it names gets its tried, resolver and escalation
|
|
// (clause 7).
|
|
func (k *Keeper) Attempt(key string, value []byte, deleted bool) error {
|
|
k.mu.Lock()
|
|
k.init()
|
|
var condition string
|
|
if old, ok := k.attempts[key]; ok {
|
|
condition = old.Condition
|
|
}
|
|
if deleted {
|
|
delete(k.attempts, key)
|
|
} else {
|
|
var a health.Attempt
|
|
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)
|
|
return nil
|
|
}
|
|
k.attempts[key], condition = a, a.Condition
|
|
}
|
|
k.mu.Unlock()
|
|
if condition == "" {
|
|
return nil
|
|
}
|
|
return k.Settle(condition)
|
|
}
|
|
|
|
// Controller takes one change of the controller's `conditions` bucket (clauses 8, 9): read until the lease write.
|
|
func (k *Keeper) Controller(key string, value []byte, deleted bool) error {
|
|
k.mu.Lock()
|
|
k.init()
|
|
if deleted {
|
|
delete(k.ctrl, key)
|
|
} else {
|
|
var c health.Condition
|
|
if err := json.Unmarshal(value, &c); err != nil {
|
|
k.mu.Unlock()
|
|
k.Logf("the controller's condition %s is not readable, so nothing is carried from it: %v", key, err)
|
|
return nil
|
|
}
|
|
k.ctrl[key] = c
|
|
}
|
|
k.mu.Unlock()
|
|
return k.Settle(key)
|
|
}
|
|
|
|
// Lease takes the controller's lease key as read (clause 8, Limits 8): once it is handed to a holder that is no
|
|
// controller, nothing more is carried from the controller's bucket, and the kinds that ended with it clear.
|
|
func (k *Keeper) Lease(away bool) error {
|
|
k.mu.Lock()
|
|
k.init()
|
|
was := k.leaseOut
|
|
k.leaseOut = away
|
|
keys := make([]string, 0, len(k.ctrl))
|
|
for key := range k.ctrl {
|
|
keys = append(keys, key)
|
|
}
|
|
k.mu.Unlock()
|
|
if was == away {
|
|
return nil
|
|
}
|
|
return k.settleAll(keys)
|
|
}
|
|
|
|
// LeaseAway says whether the controller's lease is handed away.
|
|
func (k *Keeper) LeaseAway() bool {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
return k.leaseOut
|
|
}
|
|
|
|
// Takeover takes the keeper's own take-over key as read.
|
|
func (k *Keeper) Takeover(t health.Takeover) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
k.init()
|
|
k.takeover = t
|
|
}
|
|
|
|
// Settle derives the condition key should be and writes the difference (clause 2), retried while another write moves
|
|
// it under this one.
|
|
func (k *Keeper) Settle(key string) error {
|
|
var err error
|
|
for i := 0; i < ConditionWrites; i++ {
|
|
if err = k.settleOnce(key); !errors.Is(err, stdio.ErrChanged) {
|
|
return err
|
|
}
|
|
}
|
|
return fmt.Errorf("the condition %s kept moving under this write, %d tries: %w", key, ConditionWrites, err)
|
|
}
|
|
|
|
func (k *Keeper) settleAll(keys []string) error {
|
|
sort.Strings(keys)
|
|
var problems []string
|
|
for _, key := range keys {
|
|
if err := k.Settle(key); err != nil {
|
|
problems = append(problems, err.Error())
|
|
}
|
|
}
|
|
if len(problems) > 0 {
|
|
return errors.New(strings.Join(problems, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// wanted is what the inputs say key should be: a finding of an observer, or a condition carried from the controller's
|
|
// bucket of a kind that ends with the controller, until the lease write (clause 9). Nil when nothing says it.
|
|
func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) {
|
|
observers := make([]string, 0, len(k.findings))
|
|
for o := range k.findings {
|
|
observers = append(observers, o)
|
|
}
|
|
sort.Strings(observers)
|
|
for _, o := range observers {
|
|
if f, ok := k.findings[o][key]; ok {
|
|
return &f, nil
|
|
}
|
|
}
|
|
if c, ok := k.ctrl[key]; ok && !k.leaseOut && k.endsWithController(c) {
|
|
return nil, &c
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (k *Keeper) endsWithController(c health.Condition) bool {
|
|
h, ok := k.homeOf(c.Kind)
|
|
return ok && h.Carried
|
|
}
|
|
|
|
// settleOnce is one try of Settle.
|
|
func (k *Keeper) settleOnce(key string) error {
|
|
now := k.Now().UTC()
|
|
raw, rev, open, err := k.Store.Get(ConditionsState, key)
|
|
if err != nil {
|
|
return fmt.Errorf("reading the condition %s: %w", key, err)
|
|
}
|
|
var was health.Condition
|
|
if open {
|
|
if err := json.Unmarshal(raw, &was); err != nil {
|
|
return fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
|
|
}
|
|
}
|
|
k.mu.Lock()
|
|
k.init()
|
|
f, carried := k.wanted(key)
|
|
hold, holding := k.holdFor(key, f, carried, open)
|
|
k.mu.Unlock()
|
|
|
|
if f == nil && carried == nil {
|
|
k.dropHeld(key, "its finding went while its module was delivered")
|
|
if !open {
|
|
return nil
|
|
}
|
|
return k.clear(key, was, rev, now, "its finding went")
|
|
}
|
|
if holding {
|
|
return k.keepHeld(hold)
|
|
}
|
|
k.dropHeld(key, "")
|
|
next := k.derive(key, was, open, f, carried, now)
|
|
events := transitions(was, next, open)
|
|
if len(events) == 0 && open && sameBody(was, next) {
|
|
return nil
|
|
}
|
|
if open {
|
|
_, err = k.Store.Update(ConditionsState, key, next, rev)
|
|
} else {
|
|
_, err = k.Store.Create(ConditionsState, key, next)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k.mu.Lock()
|
|
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 = k.show(next, carried != nil)
|
|
k.tell(e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (k *Keeper) show(c health.Condition, carried bool) string {
|
|
if carried {
|
|
return health.ShowController(c.Key)
|
|
}
|
|
return health.ShowKeeper(c.Key)
|
|
}
|
|
|
|
// 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, carried *health.Condition, now time.Time) health.Condition {
|
|
var c health.Condition
|
|
switch {
|
|
case carried != nil:
|
|
// Raised and cleared as the controller's bucket says, with its show and actions (Limits 9).
|
|
c = *carried
|
|
c.Epoch = 0
|
|
c.LooksUnread = ""
|
|
case open:
|
|
c = was
|
|
k.fromFinding(&c, *f, now)
|
|
default:
|
|
c = health.Condition{Key: key, Raised: now, Count: 1, Resolver: health.ResolverSelf}
|
|
k.fromFinding(&c, *f, now)
|
|
k.mu.Lock()
|
|
if before, ok := k.cleared[key]; ok && now.Sub(before.at) <= health.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.First, c.Gaps = c.Gaps[over-1].Reopened, c.Gaps[over:]
|
|
}
|
|
if before.silenced != nil && now.Before(before.silenced.Until) {
|
|
c.Silenced = before.silenced
|
|
}
|
|
c.Tried = before.tried
|
|
}
|
|
k.mu.Unlock()
|
|
}
|
|
if carried == nil {
|
|
k.overlay(&c, now)
|
|
}
|
|
if !open {
|
|
k.mu.Lock()
|
|
if line := k.outOfHold[key]; line != "" {
|
|
c.Explanation = strings.TrimSpace(c.Explanation + " " + line)
|
|
}
|
|
k.mu.Unlock()
|
|
}
|
|
if c.Silenced != nil && !now.Before(c.Silenced.Until) {
|
|
c.Silenced = nil
|
|
}
|
|
return c
|
|
}
|
|
|
|
// 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) {
|
|
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) {
|
|
c.Severity = f.Severity
|
|
}
|
|
if !escalated {
|
|
c.Resolver = f.Resolver
|
|
if c.Resolver == "" {
|
|
c.Resolver = health.ResolverSelf
|
|
}
|
|
}
|
|
if len(f.Evidence) > 0 {
|
|
c.Evidence = append([]health.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.LastObserved = f.At
|
|
}
|
|
c.Observations = len(c.Evidence)
|
|
if c.Actions == nil {
|
|
c.Actions = []health.Action{}
|
|
}
|
|
_ = now
|
|
}
|
|
|
|
// overlay writes the healers' attempts into the condition (clause 7) and, until the lease write, what the controller's
|
|
// bucket holds for the same key (clause 8): its tried, its resolver and escalation, and its silence where the keeper has
|
|
// none of its own.
|
|
func (k *Keeper) overlay(c *health.Condition, now time.Time) {
|
|
k.mu.Lock()
|
|
var mine []health.Attempt
|
|
for _, a := range k.attempts {
|
|
if a.Condition == c.Key {
|
|
mine = append(mine, a)
|
|
}
|
|
}
|
|
ctrl, carry := k.ctrl[c.Key]
|
|
carry = carry && !k.leaseOut
|
|
k.mu.Unlock()
|
|
sort.Slice(mine, func(i, j int) bool { return mine[i].Begun.Before(mine[j].Begun) })
|
|
if carry && len(ctrl.Tried) > 0 {
|
|
c.Tried = append([]health.Tried(nil), ctrl.Tried...)
|
|
if ctrl.Escalated() {
|
|
c.Resolver, c.Severity = health.ResolverOperator, health.Urgent
|
|
} else if ctrl.Resolver != "" && ctrl.Resolver != health.ResolverSelf {
|
|
c.Resolver = ctrl.Resolver
|
|
}
|
|
}
|
|
for _, a := range mine {
|
|
t := a.Tried()
|
|
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
|
|
default:
|
|
if c.Resolver != health.ResolverOperator {
|
|
c.Resolver = health.ResolverHealer(a.Healer)
|
|
}
|
|
}
|
|
}
|
|
if len(c.Tried) > health.KeptTried {
|
|
c.Tried = c.Tried[len(c.Tried)-health.KeptTried:]
|
|
}
|
|
if carry && c.Silenced == nil && ctrl.Silenced != nil && now.Before(ctrl.Silenced.Until) {
|
|
c.Silenced = ctrl.Silenced
|
|
}
|
|
}
|
|
|
|
func containsTried(list []health.Tried, t health.Tried) bool {
|
|
for _, x := range list {
|
|
if x.At.Equal(t.At) && x.By == t.By && x.What == t.What {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 {
|
|
if !open {
|
|
change := health.ChangeRaised
|
|
if now.Count > 1 {
|
|
change = health.ChangeReopened
|
|
}
|
|
return []health.Event{{Change: change}}
|
|
}
|
|
var out []health.Event
|
|
if was.Severity != now.Severity {
|
|
out = append(out, health.Event{Change: health.ChangeSeverity, Was: string(was.Severity)})
|
|
}
|
|
if was.Resolver != now.Resolver {
|
|
out = append(out, health.Event{Change: health.ChangeResolver, Was: was.Resolver})
|
|
}
|
|
switch {
|
|
case was.Silenced == nil && now.Silenced != nil:
|
|
out = append(out, health.Event{Change: health.ChangeSilenced, Why: now.Silenced.Why})
|
|
case was.Silenced != nil && now.Silenced == nil:
|
|
out = append(out, health.Event{Change: health.ChangeUnsilenced})
|
|
}
|
|
if was.Needs != now.Needs || !sameActions(was.Actions, now.Actions) {
|
|
out = append(out, health.Event{Change: health.ChangeWords, Was: was.Needs})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sameActions(a, b []health.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 {
|
|
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 {
|
|
// 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
|
|
}
|
|
if err := k.Store.Delete(ConditionsState, key); err != nil {
|
|
return err
|
|
}
|
|
k.mu.Lock()
|
|
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: k.show(was, false)})
|
|
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) {
|
|
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 {
|
|
k.Logf("the %s of %s could not be kept in history: %v", e.Change, e.Key, err)
|
|
}
|
|
if k.Say == nil {
|
|
return
|
|
}
|
|
if err := k.Say(e); err != nil {
|
|
k.Logf("the %s of %s could NOT be said on the seat: %v", e.Change, e.Key, err)
|
|
}
|
|
}
|
|
|
|
// Open is every open condition, ordered as `conditions` says them.
|
|
func (k *Keeper) Open() ([]health.Condition, error) {
|
|
keys, err := k.Store.Keys(ConditionsState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []health.Condition
|
|
for _, key := range keys {
|
|
raw, _, found, err := k.Store.Get(ConditionsState, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found || string(raw) == "null" {
|
|
continue
|
|
}
|
|
var c health.Condition
|
|
if err := json.Unmarshal(raw, &c); 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)
|
|
return out, nil
|
|
}
|
|
|
|
// Resync settles every key any input names or any open condition holds (clause 12): what differs is raised or cleared.
|
|
func (k *Keeper) Resync() (int, error) {
|
|
open, err := k.Open()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, c := range open {
|
|
seen[c.Key] = true
|
|
}
|
|
k.mu.Lock()
|
|
k.init()
|
|
for o, fs := range k.findings {
|
|
if k.blind[o] != "" {
|
|
continue
|
|
}
|
|
for key := range fs {
|
|
seen[key] = true
|
|
}
|
|
}
|
|
for key := range k.ctrl {
|
|
seen[key] = true
|
|
}
|
|
k.mu.Unlock()
|
|
keys := make([]string, 0, len(seen))
|
|
for key := range seen {
|
|
keys = append(keys, key)
|
|
}
|
|
return len(keys), k.settleAll(keys)
|
|
}
|
|
|
|
// 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) {
|
|
if strings.TrimSpace(why) == "" {
|
|
return health.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)
|
|
}
|
|
for i := 0; i < ConditionWrites; i++ {
|
|
raw, rev, found, err := k.Store.Get(ConditionsState, key)
|
|
if err != nil {
|
|
return health.Condition{}, err
|
|
}
|
|
if !found || string(raw) == "null" {
|
|
return health.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
|
|
}
|
|
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) {
|
|
continue
|
|
} else if err != nil {
|
|
return health.Condition{}, err
|
|
}
|
|
k.tell(health.Event{Condition: c, Event: health.EventChanged, At: now, Change: health.ChangeSilenced,
|
|
Why: why + " (" + by + ")", Show: health.ShowKeeper(key)})
|
|
return c, nil
|
|
}
|
|
return health.Condition{}, fmt.Errorf("the condition %s kept moving under the silence", key)
|
|
}
|