G findings4 watchman: the watchman's findings are the seat's generated type, written through mesh-sdk's finding writer; the resolved words go
Builds: design 47 G §689, §686 (A-3), §461, §384, A-13, A-14, A-22 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"mesh-delivery-watchman/internal/watch"
|
||||
)
|
||||
|
||||
type findingWriterOf = views.Writer[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]
|
||||
|
||||
// heldFindings is the watchman's findings through mesh-sdk's finding writer (design 47 G §461, §689): every finding the
|
||||
// watchman holds, by its own key, which the writer states at its start and when the keeper asks, and writes and clears as the
|
||||
// watchman puts and deletes them. It has a lock of its own, never the watchman's: the writer calls observe under its own
|
||||
// lock while the watchman writes under its.
|
||||
type heldFindings struct {
|
||||
mu sync.Mutex
|
||||
held map[string]condseat.Finding
|
||||
w *findingWriterOf
|
||||
}
|
||||
|
||||
// seedFindings reads the findings that stand in the state `findings` at the start, by the watchman's key of each.
|
||||
func seedFindings(state process.State) (*heldFindings, map[string][]byte, error) {
|
||||
h := &heldFindings{held: map[string]condseat.Finding{}}
|
||||
seeds := map[string][]byte{}
|
||||
keys, err := state.Keys()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, k := range keys {
|
||||
e, err := state.Get(k)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
f, err := watch.DecodeFinding(e.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h.held[f.Key()] = f.Contract()
|
||||
seeds[f.Key()] = e.Value
|
||||
}
|
||||
return h, seeds, nil
|
||||
}
|
||||
|
||||
// start makes and starts the writer: it states what is held at once, so the seeds are made before it.
|
||||
func (h *heldFindings) start(ctx context.Context, cred process.Account, conn *nats.Conn, js jetstream.JetStream, state process.State, logf func(string, ...any)) error {
|
||||
w, err := views.NewWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{
|
||||
Module: cred.Module, State: state, Node: cred.Node, JS: js, Conn: conn, Logf: logf,
|
||||
Observe: func(context.Context) (views.Observed[condseat.Finding], error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
var o views.Observed[condseat.Finding]
|
||||
for _, f := range h.held {
|
||||
o.Findings = append(o.Findings, f)
|
||||
}
|
||||
return o, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.w = w
|
||||
// A writer that cannot start (the stream of its asks is not there yet) is said; its findings are then kept in the
|
||||
// state alone and said by the next start (as the node-engine's is).
|
||||
if err := w.Start(ctx); err != nil {
|
||||
logf("the finding writer cannot start, its findings are kept in the state alone: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put writes the finding the watchman holds under key.
|
||||
func (h *heldFindings) Put(ctx context.Context, key string, value []byte) error {
|
||||
f, err := watch.DecodeFinding(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := f.Contract()
|
||||
h.mu.Lock()
|
||||
h.held[key] = c
|
||||
h.mu.Unlock()
|
||||
return h.w.Write(ctx, c)
|
||||
}
|
||||
|
||||
// Delete clears the finding the watchman held under key.
|
||||
func (h *heldFindings) Delete(ctx context.Context, key string) error {
|
||||
h.mu.Lock()
|
||||
c, ok := h.held[key]
|
||||
delete(h.held, key)
|
||||
h.mu.Unlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return h.w.Clear(ctx, c.Scope, c.ID, c.Kind, "the watchman no longer observes it")
|
||||
}
|
||||
@@ -71,6 +71,14 @@ func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *
|
||||
viewstest.Serve(t, ctx, js, nc, watch.IndexerModule, modulesState, watch.IndexerModule+".indexer_read_modules", "n1_"+watch.WatchmanModule)
|
||||
}
|
||||
|
||||
// findingsStream is the watchman's own stream of its findings' events (design 47 G §383), which its finding writer says into.
|
||||
func findingsStream(t *testing.T, ctx context.Context, js jetstream.JetStream) {
|
||||
t.Helper()
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "WATCHMAN_FINDINGS", Subjects: []string{"mesh.stream." + watch.WatchmanModule + ".event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchmanWhole starts the program the way the node does (its entry, run, with its environment and a broker file)
|
||||
// against an embedded bus with the system exports and JetStream, and proves the four things the issue names.
|
||||
func TestWatchmanWhole(t *testing.T) { // 1135/AC8
|
||||
@@ -132,6 +140,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8
|
||||
kvs[c.Bucket] = kv
|
||||
}
|
||||
serveOwners(t, ctx, js, ctl)
|
||||
findingsStream(t, ctx, js)
|
||||
|
||||
// The controller's consumer on DEAD_LETTER_NOTICES, the dead letters' streams and a source stream with one message a
|
||||
// consumer will give up on (1952/WM5); no lease bucket at all (issue 1126).
|
||||
@@ -383,6 +392,7 @@ func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909
|
||||
kvs[c.Bucket] = kv
|
||||
}
|
||||
serveOwners(t, ctx, js, ctl)
|
||||
findingsStream(t, ctx, js)
|
||||
const id = "m0000009ffffff"
|
||||
body, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_test", Node: "n1", State: buildstep.StepRunning, Started: time.Now().UTC()})
|
||||
if err != nil {
|
||||
@@ -470,6 +480,7 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) {
|
||||
kvs[c.Bucket] = kv
|
||||
}
|
||||
serveOwners(t, ctx, js, ctl)
|
||||
findingsStream(t, ctx, js)
|
||||
fpNew := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64)
|
||||
fpOld := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64)
|
||||
putWant := func(node, module, fp string) {
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
@@ -51,12 +49,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// kvOut writes the watchman's four buckets.
|
||||
// kvOut writes the watchman's three buckets and its findings, which go through mesh-sdk's finding writer.
|
||||
type kvOut struct {
|
||||
ctx context.Context
|
||||
presence, pulse, stuk, findings jetstream.KeyValue
|
||||
js jetstream.JetStream
|
||||
cred process.Account
|
||||
ctx context.Context
|
||||
presence, pulse, stuk jetstream.KeyValue
|
||||
findings *heldFindings
|
||||
js jetstream.JetStream
|
||||
cred process.Account
|
||||
}
|
||||
|
||||
// Say publishes an event of the seat mesh-watchdogs, said under the watchman's own account (design 47 G §389, §436).
|
||||
@@ -74,10 +73,7 @@ func (o *kvOut) Say(subject string, body []byte, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *kvOut) PutFinding(key string, value []byte) error {
|
||||
_, err := o.findings.Put(o.ctx, key, value)
|
||||
return err
|
||||
}
|
||||
func (o *kvOut) PutFinding(key string, value []byte) error { return o.findings.Put(o.ctx, key, value) }
|
||||
|
||||
func (o *kvOut) DeleteFinding(key string) error { return o.findings.Delete(o.ctx, key) }
|
||||
|
||||
@@ -233,11 +229,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// Its own presence, pulses and stuck rows are what it keeps: each stops the start, naming its grant. The assigner's wants
|
||||
// and the node-engine's haves only make the pulses of a move: without the haves the watchman watches presence and the
|
||||
// build seats' steps, and says that moves are not watched (issue 1945).
|
||||
var kvs struct{ presence, pulse, stuck, findings, haves jetstream.KeyValue }
|
||||
var kvs struct{ presence, pulse, stuck, haves jetstream.KeyValue }
|
||||
for _, b := range []struct {
|
||||
name string
|
||||
into *jetstream.KeyValue
|
||||
}{{watch.PresenceBucket, &kvs.presence}, {watch.PulseBucket, &kvs.pulse}, {watch.StuckBucket, &kvs.stuck}, {watch.FindingsBucket, &kvs.findings}} {
|
||||
}{{watch.PresenceBucket, &kvs.presence}, {watch.PulseBucket, &kvs.pulse}, {watch.StuckBucket, &kvs.stuck}} {
|
||||
if *b.into, err = process.EssentialBucket(ctx, js, "watchman", b.name, "say who is online or what stopped moving"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -255,10 +251,19 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// The module has one durable consumer of EVENTS: one hub pulls it for the views reader, the indexer's take-in events
|
||||
// and the seats' events, started once every part is registered.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
// Its findings: those that stand in its state are read first, so the writer's statement at its start says them and
|
||||
// deletes none (the watchman reconciles them below, once it has read what it observes).
|
||||
findingsState := calls.State("findings")
|
||||
held, seeds, err := seedFindings(findingsState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the watchman's findings cannot be read: %w", err)
|
||||
}
|
||||
if err := held.start(ctx, cred, conn, js, findingsState, logger.Printf); err != nil {
|
||||
return fmt.Errorf("the watchman's finding writer: %w", err)
|
||||
}
|
||||
defer held.w.Close()
|
||||
reader := &views.ProcessReader{Module: watch.WatchmanModule, Call: calls.Call, Logf: logger.Printf, Hub: hub,
|
||||
Findings: func() views.FindingWriter {
|
||||
return sdkfindings.Writer(ctx, func() jetstream.KeyValue { return kvs.findings })
|
||||
}}
|
||||
Findings: func() views.FindingWriter { return views.WriterFindings{Writer: held.w} }}
|
||||
wants, err := reader.Follow(watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -268,7 +273,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
}
|
||||
|
||||
w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: kvs.findings, js: js, cred: cred}, time.Now)
|
||||
w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: held, js: js, cred: cred}, time.Now)
|
||||
w.Logf(logger.Printf)
|
||||
// What the wants and the modules hold is ended by their own late read (watch.ReadLate), never by the start's end.
|
||||
w.Awaits(watch.AssignerWantsBucket)
|
||||
@@ -282,6 +287,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
w.SetBound(d)
|
||||
}
|
||||
|
||||
for key, value := range seeds {
|
||||
w.SeedFinding(key, value)
|
||||
}
|
||||
// What stands now: presence and stuck as written, pulses as the server holds them, then the checks.
|
||||
for _, s := range []struct {
|
||||
kv jetstream.KeyValue
|
||||
@@ -292,11 +300,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
w.SeedPresence(e.Key(), p.Online, e.Created())
|
||||
}
|
||||
}},
|
||||
{kvs.findings, func(e jetstream.KeyValueEntry) {
|
||||
if opOf(e) == watch.Put {
|
||||
w.SeedFinding(e.Key(), e.Value())
|
||||
}
|
||||
}},
|
||||
{kvs.stuck, func(e jetstream.KeyValueEntry) {
|
||||
if opOf(e) == watch.Put {
|
||||
w.SeedStuck(e.Key(), e.Value())
|
||||
|
||||
@@ -47,7 +47,7 @@ replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
require git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect
|
||||
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/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
|
||||
@@ -83,8 +83,7 @@ func (w *Watchman) Accepted(a meshcatalog.ManifestAcceptedEvent) {
|
||||
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"}})
|
||||
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"}})
|
||||
}
|
||||
for _, local := range w.busStateEntriesOf(a.Module) {
|
||||
if asked[local] {
|
||||
@@ -151,8 +150,7 @@ func (w *Watchman) BrokerRefused(s BrokerSaid) {
|
||||
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"})
|
||||
Needs: "a pull request that mends the entry in the manifest of " + s.Module + ", or the bus module's cause fixed"})
|
||||
}
|
||||
|
||||
// busStateEntriesOf are the entries of module this watchman waits on or holds a finding of, by local name, sorted.
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
|
||||
)
|
||||
|
||||
// The watchman is an observer of the seat mesh-watchdogs (novox/hq ADR 0564 §2, §4): what it observes wrong it writes as a
|
||||
// finding, one key `<scope>.<id>.<kind>` of its own bucket `findings` in the seat mesh-conditions' layout (meshconditionsclient), only when what
|
||||
// it says changes, and it deletes the key when it no longer observes it. The condition keeper (mesh-conditions) raises and
|
||||
// finding, one key `<scope>.<id>.<kind>` of its own state `findings` in the seat mesh-conditions' generated layout, written
|
||||
// through mesh-sdk's finding writer (design 47 G §689), only when what it says changes, and it deletes the key when it no longer observes it. The condition keeper (mesh-conditions) raises and
|
||||
// clears the conditions from them; the watchman writes no condition and reads none (Limits 11).
|
||||
|
||||
// FindingsBucket is the watchman's findings.
|
||||
var FindingsBucket = mcc.FindingsBucket(Observer)
|
||||
var FindingsBucket = Observer + "_findings"
|
||||
|
||||
// Observer is the name a finding of the watchman carries.
|
||||
const Observer = "mesh-delivery-watchman"
|
||||
@@ -43,7 +46,7 @@ type Finding struct {
|
||||
Severity string
|
||||
Summary, Said string
|
||||
Headline, Explanation string
|
||||
Needs, Resolved string
|
||||
Needs string
|
||||
Source string
|
||||
Observer string
|
||||
// Since is when what it says began, as the watchman saw it: the finding's time and its observation's, so the same
|
||||
@@ -63,22 +66,42 @@ func (f Finding) Key() string {
|
||||
return FindingKey(f.Scope, f.ID, token)
|
||||
}
|
||||
|
||||
// EncodeFinding is f in the seat's layout, refusing one the keeper could not make a condition of.
|
||||
func EncodeFinding(f Finding) ([]byte, error) {
|
||||
return mcc.EncodeFinding(mcc.Observation{Scope: f.Scope, ID: f.ID, Kind: f.Kind, Ref: f.Token, Machine: f.Machine, Also: f.Also,
|
||||
Severity: f.Severity, Summary: f.Summary, Said: f.Said, Headline: f.Headline, Explanation: f.Explanation,
|
||||
Resolved: f.Resolved, Needs: f.Needs, Source: f.Source}.Finding(Observer, "", f.Since))
|
||||
// Contract is f as the seat mesh-conditions' generated finding (design 47 G §686, §689): a ref, where the finding has a
|
||||
// token other than its kind, goes into the id as `<id>.<ref>` (A-14), and is kept in `ref` so the finding reads back as
|
||||
// it was written. The resolved words are not a field of the seat (A-22), and its `at` is its `since`, so the same thing
|
||||
// observed again writes the same bytes.
|
||||
func (f Finding) Contract() condseat.Finding {
|
||||
id, ref := f.ID, ""
|
||||
if f.Token != "" && f.Token != f.Kind {
|
||||
ref = f.Token
|
||||
id = f.ID + "." + ref
|
||||
}
|
||||
v := condseat.Finding{Scope: f.Scope, ID: id, Kind: f.Kind, Ref: ref, Severity: f.Severity, Summary: f.Summary,
|
||||
Headline: f.Headline, Explanation: f.Explanation, Needs: f.Needs, Source: f.Source, Observer: Observer,
|
||||
At: f.Since.UTC(), Since: f.Since.UTC(),
|
||||
Subject: condseat.Subject{Scope: f.Scope, ID: id, Machine: f.Machine, Also: f.Also}}
|
||||
if f.Said != "" {
|
||||
v.Evidence = []condseat.Evidence{{At: f.Since.UTC(), Said: f.Said}}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// EncodeFinding is f in the seat's generated layout.
|
||||
func EncodeFinding(f Finding) ([]byte, error) { return json.Marshal(f.Contract()) }
|
||||
|
||||
// DecodeFinding reads a finding the watchman wrote.
|
||||
func DecodeFinding(data []byte) (Finding, error) {
|
||||
h, err := mcc.DecodeFinding(data)
|
||||
if err != nil {
|
||||
var h condseat.Finding
|
||||
if err := json.Unmarshal(data, &h); err != nil {
|
||||
return Finding{}, err
|
||||
}
|
||||
f := Finding{Scope: h.Subject.Scope, ID: h.Subject.ID, Token: h.Ref, Kind: h.Kind, Machine: h.Subject.Machine,
|
||||
id := h.ID
|
||||
if h.Ref != "" {
|
||||
id = strings.TrimSuffix(id, "."+h.Ref)
|
||||
}
|
||||
f := Finding{Scope: h.Scope, ID: id, Token: h.Ref, Kind: h.Kind, Machine: h.Subject.Machine,
|
||||
Also: h.Subject.Also, Severity: h.Severity, Summary: h.Summary, Headline: h.Headline, Explanation: h.Explanation,
|
||||
Needs: h.Needs, Resolved: h.Resolved, Source: h.Source, Observer: h.Observer, Since: h.At}
|
||||
Needs: h.Needs, Source: h.Source, Observer: h.Observer, Since: h.Since}
|
||||
if len(h.Evidence) > 0 {
|
||||
f.Said = h.Evidence[0].Said
|
||||
}
|
||||
@@ -163,7 +186,7 @@ func (w *Watchman) NotHeard(family, consequence string, why error) {
|
||||
w.say("%s", summary)
|
||||
w.find(Finding{Scope: ScopeModule, ID: heard.ID(Observer, family), Token: heard.Kind, Kind: heard.Kind, Severity: Warning,
|
||||
Source: Observer + "." + heard.Kind, Since: w.now().UTC().Truncate(time.Second), Summary: summary, Headline: headline,
|
||||
Explanation: explanation, Resolved: "the watchman's consumer of " + family + " is bound"})
|
||||
Explanation: explanation})
|
||||
}
|
||||
|
||||
// Heard ends the finding that the watchman does not hear family.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
@@ -325,7 +326,8 @@ func TestTheWatchmansFindings(t *testing.T) {
|
||||
case !row.noState && row.want == "" && stands:
|
||||
t.Errorf("%s: the finding stands once bound", row.name)
|
||||
case row.want != "":
|
||||
f, err := mcc.DecodeFinding(raw)
|
||||
var f condseat.Finding
|
||||
err := json.Unmarshal(raw, &f)
|
||||
// Review round 2: it says it clears at the part's next start, since Bind runs at the start.
|
||||
if err != nil || f.Kind != heard.Kind || f.Summary != row.want || f.Observer != "mesh-delivery-indexer" || !strings.Contains(f.Explanation, "next start") {
|
||||
t.Errorf("%s: the finding %+v (%v), want summary %q", row.name, f, err, row.want)
|
||||
@@ -820,3 +822,33 @@ func takeInBody(t *testing.T, kind, key string, since time.Time) []byte {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Design 47 G §689, A-14, A-22: a token other than the kind goes into the contract finding's id and reads back as it was
|
||||
// written; the finding's time is its `since`; the resolved words are not on the wire.
|
||||
func TestAFindingIsTheContractsWithItsRefInItsID(t *testing.T) {
|
||||
since := time.Date(2026, 11, 11, 20, 0, 0, 0, time.UTC)
|
||||
for _, f := range []Finding{
|
||||
{Scope: ScopeModule, ID: "web.ace", Token: "unhealthy", Kind: KindUnhealthy, Machine: "ace", Severity: Warning, Source: "health", Since: since, Summary: "s", Said: "said"},
|
||||
{Scope: ScopeModule, ID: "web", Token: KindBusStateOverdue + "-state", Kind: KindBusStateOverdue, Severity: Warning, Source: "bus", Since: since, Summary: "s"},
|
||||
{Scope: ScopeMachine, ID: "ace", Kind: KindMachineUnits, Severity: Warning, Source: "units", Since: since, Summary: "s", Needs: "mend"},
|
||||
} {
|
||||
c := f.Contract()
|
||||
wantID := f.ID
|
||||
if f.Token != "" && f.Token != f.Kind {
|
||||
wantID = f.ID + "." + f.Token
|
||||
}
|
||||
if c.ID != wantID || c.Kind != f.Kind || !c.At.Equal(since) || !c.Since.Equal(since) || c.Subject.ID != wantID {
|
||||
t.Errorf("%+v: contract %+v, want id %q, kind %q, at and since %v", f, c, wantID, f.Kind, since)
|
||||
}
|
||||
raw, err := EncodeFinding(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if back, err := DecodeFinding(raw); err != nil || back.ID != f.ID || back.Token != f.Token || back.Said != f.Said || back.Needs != f.Needs {
|
||||
t.Errorf("%+v read back as %+v (%v)", f, back, err)
|
||||
}
|
||||
if strings.Contains(string(raw), "esolved") {
|
||||
t.Errorf("%+v: resolved words are on the wire: %s", f, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +198,7 @@ func unitsOf(node string, u *nodeengine.UnitsHealth) (Finding, bool) {
|
||||
"mend or remove it there, or have a module place it", node, len(u.Failed), strings.Join(names, ", ")),
|
||||
Said: strings.Join(said, "; "),
|
||||
Headline: strings.ToUpper(failed[:1]) + failed[1:] + " on " + node,
|
||||
Needs: fmt.Sprintf("mend or remove them on %s, or silence this if they do not matter.", node),
|
||||
Resolved: "No failed services on " + node + " any more"}, true
|
||||
Needs: fmt.Sprintf("mend or remove them on %s, or silence this if they do not matter.", node)}, true
|
||||
}
|
||||
|
||||
// reloginOf is one finding per module whose accounts wait for a new login.
|
||||
@@ -251,8 +250,7 @@ func foundOf(node string, dirs []nodeengine.NodeDirectory) []Finding {
|
||||
Headline: fmt.Sprintf("%s waits for a directory on %s", m, node),
|
||||
Explanation: fmt.Sprintf("Needs you: hand the directory over from the control-node, as the operator; the details name it "+
|
||||
"and the line to type. A directory of %s was already on %s, with another owner or mode than %s declares.", m, node, m),
|
||||
Needs: "hand the directory over from the control-node, as the operator; the details name it and the line to type.",
|
||||
Resolved: fmt.Sprintf("%s's directory on %s is the mesh's", m, node)})
|
||||
Needs: "hand the directory over from the control-node, as the operator; the details name it and the line to type."})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -266,8 +264,7 @@ func fallbackOf(node string, f *nodeengine.LoginFallback) (Finding, bool) {
|
||||
return Finding{Scope: ScopeMachine, ID: node, Token: KindAccountFallback, Kind: KindAccountFallback, Machine: node, Severity: Warning,
|
||||
Source: "health-report", Since: f.At, Summary: said, Said: said,
|
||||
Headline: fmt.Sprintf("%s fell back to its old bus login", node),
|
||||
Needs: fmt.Sprintf("abandon the rotation of %s's account, or rotate it again once its new login authenticates.", node),
|
||||
Resolved: fmt.Sprintf("%s no longer falls back to its old bus login", node)}, true
|
||||
Needs: fmt.Sprintf("abandon the rotation of %s's account, or rotate it again once its new login authenticates.", node)}, true
|
||||
}
|
||||
|
||||
func orModule(m string) string {
|
||||
|
||||
@@ -763,7 +763,7 @@ func (w *Watchman) applyCheck(node string) {
|
||||
}
|
||||
}
|
||||
f := Finding{Scope: ScopeMachine, ID: node, Kind: KindApplyFailing, Machine: node, Severity: Warning, Source: "S21", Since: since,
|
||||
Said: strings.Join(words, "; "), Resolved: node + " applies what it is sent again", Headline: node + " is not applying its update"}
|
||||
Said: strings.Join(words, "; "), Headline: node + " is not applying its update"}
|
||||
switch {
|
||||
case len(applied) == 0:
|
||||
f.Summary = fmt.Sprintf("%s's provisioner has failed to place the files of %s since %s (bound %s)", node,
|
||||
@@ -845,7 +845,6 @@ func (w *Watchman) Closed(kind string, c BusClosure) {
|
||||
f.Headline = clip(capital(who)+" was too slow for the bus", 60)
|
||||
f.Explanation = capital(fmt.Sprintf("%s could not keep up with what the bus sent it, so the bus closed its connection (%s "+
|
||||
"in the last hour) and what was on its way was lost. It connects again by itself; this clears an hour after the last time.", who, count))
|
||||
f.Resolved = "Resolved: " + who + " kept up for an hour"
|
||||
} else {
|
||||
reason := strings.ToLower(c.Reason)
|
||||
f.Said = fmt.Sprintf("the bus closed %s's connection %d: %s", c.User, c.CID, c.Reason)
|
||||
@@ -854,7 +853,6 @@ func (w *Watchman) Closed(kind string, c BusClosure) {
|
||||
f.Headline = clip(capital(who)+" was cut off by the bus", 60)
|
||||
f.Explanation = capital(fmt.Sprintf("The bus closed the connection of %s because it broke the bus's rules (%s, %s in the "+
|
||||
"last hour), so what it was sending was lost. This clears an hour after the last time.", who, reason, count))
|
||||
f.Resolved = "Resolved: " + who + " kept to the bus's rules for an hour"
|
||||
}
|
||||
w.find(f)
|
||||
if quiet := AdvisoryQuiet - w.now().Sub(last); quiet > 0 {
|
||||
|
||||
Reference in New Issue
Block a user