Files
mesh-delivery/modules/mesh-delivery-watchman/internal/watch/busstate.go
T

204 lines
8.8 KiB
Go

package watch
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// **A refused or overdue creation of bus state is loud** (novox/hq ADR 0595 §4; design 25 § 12.2; DLV-W1). The watchman
// hears, through its `consumes`, the indexer's `manifest-accepted` (which carries the module's bus-state entries whole)
// and the bus module's own `created` and `refused`. An entry an accepted manifest asks for that has no `created` or
// `refused` within OverdueBound is the finding bus-state-overdue, naming the module and the entry; a `refused` is the
// finding bus-state-refused, with why. A later `created` clears both, and so does a later accepted manifest of that
// module that no longer asks for the entry. Never the bus module's to raise: it cannot while it is down.
//
// The bound is a row of the signals table: a pulse key per entry with OverdueBound to live, whose expiry is the finding,
// so no loop looks and a pulse that stood across a restart still acts. Both findings outlive a restart (signalKind),
// since what ends them is heard, not read.
//
// The bus module says `created` once per entry, so an entry it said created is not waited on again when a later manifest
// of its module is accepted with the same entry (review round 1): its mark, a pulse key with no time to live under the
// row bus-state-created, is read back at a restart (SeedSignal), and goes when a refused is heard or a manifest no
// longer asks for the entry, so a later ask of it waits again.
// OverdueBound is how long an accepted entry may wait for the bus module's created or refused.
const OverdueBound = 5 * time.Minute
// The kinds, and the signals table's row.
const (
KindBusStateOverdue = "bus-state-overdue"
KindBusStateRefused = "bus-state-refused"
rowBusState = "bus-state"
rowBusCreated = "bus-state-created"
)
// BrokerSaid is the part of the bus module's created or refused the watchman reads: the module, the entry, and why a
// create was refused. It stands until the seat mesh-broker's definition is generated in mesh-contracts.
type BrokerSaid struct {
Module string `json:"module"`
// Local is the entry: the contract's `local-name` (mesh-contracts meshbroker.Said), or `local` where a body names it so.
Local string `json:"local,omitempty"`
LocalName string `json:"local-name,omitempty"`
Why string `json:"why,omitempty"`
}
// busStateID is the signal's and the findings' id of one entry: `<module>.<local>`.
func busStateID(module, local string) string { return module + "." + local }
func overdueKey(module, local string) string {
return FindingKey(ScopeModule, module, KindBusStateOverdue+"-"+local)
}
func refusedKey(module, local string) string {
return FindingKey(ScopeModule, module, KindBusStateRefused+"-"+local)
}
// Accepted is the indexer's manifest-accepted heard: each entry it asks for waits OverdueBound for the bus module's word,
// and every entry of the module this watchman waits on or holds a finding of that it no longer asks for is ended.
func (w *Watchman) Accepted(a meshcatalog.ManifestAcceptedEvent) {
w.mu.Lock()
defer w.mu.Unlock()
asked := map[string]bool{}
for _, e := range a.BusState {
if e.Name == "" {
continue
}
asked[e.Name] = true
if w.busCreated[signalKey(rowBusCreated, busStateID(a.Module, e.Name))] {
continue // said created already, and created is said once per entry: nothing to wait for
}
// One refused since an earlier verdict waits again: a new verdict asks the bus module again.
w.signal(rowBusState, busStateID(a.Module, e.Name), OverdueBound, signalDue{Do: dueFind, Finding: Finding{
Scope: ScopeModule, ID: a.Module, Token: KindBusStateOverdue + "-" + e.Name, Kind: KindBusStateOverdue,
Severity: Warning, Source: rowBusState, Since: w.now().Add(OverdueBound).UTC().Truncate(time.Second),
Summary: fmt.Sprintf("the bus module said nothing of %s's %s (%s) within %s of its accepted manifest %s", a.Module,
e.Name, e.Storage, OverdueBound, short(a.Digest)),
Headline: "The bus state " + e.Name + " of " + a.Module + " was not created",
Explanation: "The indexer accepted a manifest of " + a.Module + " asking the bus module for " + e.Name + ", and the bus " +
"module said neither created nor refused within " + OverdueBound.String() + ": it is down, or behind. " + a.Module +
" fails at its start while the entry's binding is missing (novox/hq ADR 0595 §1).",
Needs: "a look at the bus module's health and its process's log; nats_first_view_again asks it to read the verdicts again",
Resolved: "the bus module said " + e.Name + " of " + a.Module + " created, or a later manifest no longer asks for it"}})
}
for _, local := range w.busStateEntriesOf(a.Module) {
if asked[local] {
continue
}
w.unsignal(rowBusState, busStateID(a.Module, local))
w.unfind(overdueKey(a.Module, local))
w.unfind(refusedKey(a.Module, local))
w.uncreated(a.Module, local)
}
}
// markCreated keeps that the bus module said module's local created, on the pulse bucket with no time to live.
func (w *Watchman) markCreated(module, local string) {
pk := signalKey(rowBusCreated, busStateID(module, local))
owes, _ := json.Marshal(signalDue{Do: dueCreated})
value, err := meshwatchdogsclient.EncodePulse(meshwatchdogsclient.Pulse{Bucket: signalBucket, Key: strings.TrimPrefix(pk, signalBucket+"."), Owes: string(owes)})
if err == nil {
err = w.out.PutPulse(pk, value, 0)
}
if err != nil {
w.say("cannot keep that %s of %s was created (%v): its next accepted manifest waits on it again", local, module, err)
}
w.busCreated[pk] = true
}
// uncreated drops the mark of module's local said created, so its next ask waits again.
func (w *Watchman) uncreated(module, local string) {
pk := signalKey(rowBusCreated, busStateID(module, local))
if !w.busCreated[pk] {
return
}
delete(w.busCreated, pk)
if err := w.out.DeletePulse(pk); err != nil {
w.say("cannot delete the mark %s: %v", pk, err)
}
}
// BrokerCreated is the bus module's created heard: the entry's wait ends, and its findings with it.
func (w *Watchman) BrokerCreated(s BrokerSaid) {
w.mu.Lock()
defer w.mu.Unlock()
w.unsignal(rowBusState, busStateID(s.Module, s.Local))
w.unfind(overdueKey(s.Module, s.Local))
w.unfind(refusedKey(s.Module, s.Local))
w.markCreated(s.Module, s.Local)
}
// BrokerRefused is the bus module's refused heard: the entry's wait ends in the finding bus-state-refused, until a later
// created.
func (w *Watchman) BrokerRefused(s BrokerSaid) {
w.mu.Lock()
defer w.mu.Unlock()
w.unsignal(rowBusState, busStateID(s.Module, s.Local))
w.unfind(overdueKey(s.Module, s.Local))
w.uncreated(s.Module, s.Local)
why := strings.TrimSpace(s.Why)
if why == "" {
why = "it gave no reason"
}
w.find(Finding{Scope: ScopeModule, ID: s.Module, Token: KindBusStateRefused + "-" + s.Local, Kind: KindBusStateRefused,
Severity: Urgent, Source: rowBusState, Since: w.now().UTC().Truncate(time.Second), Said: why,
Summary: fmt.Sprintf("the bus module refused to create %s's %s: %s", s.Module, s.Local, why),
Headline: "The bus state " + s.Local + " of " + s.Module + " was refused",
Explanation: "The bus module could not create " + s.Local + " for " + s.Module + ", so no binding is handed and " + s.Module +
" fails at its start, naming the entry (novox/hq ADR 0595 §1): " + why,
Needs: "a pull request that mends the entry in the manifest of " + s.Module + ", or the bus module's cause fixed",
Resolved: "the bus module said " + s.Local + " of " + s.Module + " created"})
}
// busStateEntriesOf are the entries of module this watchman waits on or holds a finding of, by local name, sorted.
func (w *Watchman) busStateEntriesOf(module string) []string {
seen := map[string]bool{}
prefix := signalKey(rowBusState, module) + "."
for pk := range w.signals {
if local, ok := strings.CutPrefix(pk, prefix); ok && !strings.Contains(local, ".") {
seen[local] = true
}
}
for _, kind := range []string{KindBusStateOverdue, KindBusStateRefused} {
p := FindingKey(ScopeModule, module, kind+"-")
for key := range w.findings {
if local, ok := strings.CutPrefix(key, p); ok {
seen[local] = true
}
}
}
out := make([]string, 0, len(seen))
for l := range seen {
out = append(out, l)
}
sort.Strings(out)
return out
}
// ReadBrokerSaid reads the bus module's created or refused.
func ReadBrokerSaid(body []byte) (BrokerSaid, error) {
var s BrokerSaid
if err := json.Unmarshal(body, &s); err != nil {
return BrokerSaid{}, fmt.Errorf("the bus module's word cannot be read: %w", err)
}
if s.Local == "" {
s.Local = s.LocalName
}
if s.Module == "" || s.Local == "" {
return BrokerSaid{}, fmt.Errorf("the bus module's word names no module or no entry")
}
return s, nil
}
func short(digest string) string {
if len(digest) > 12 {
return digest[:12]
}
return digest
}