Every look rewrote the condition's value in the conditions bucket, because the value carried the count of looks, the last look and the newest evidence: 131 647 writes in four days for twelve open conditions, against ADR 0289's rule that a bucket is written only when what it describes changed. The looks now live in the controller's store (migration 0095, condition_looks), the bucket holds what a condition says and is written only when that changes, and the conditions verb and the condition events put the looks back.
88 lines
2.9 KiB
Go
88 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// **Looks at an unchanged condition write the bucket once, and `conditions` still says how often and when
|
|
// it was seen** (novox/hq issue 420): the looks are in the controller's store, and the verb, run as a
|
|
// process of its own, reads them from there. On 2026-10-10 one open condition had rewritten the bucket 7 003
|
|
// times in two days.
|
|
func TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds(t *testing.T) {
|
|
inv := inventory.ForTest(t)
|
|
bus := testbus.Start(t)
|
|
serving, err := broker.Dial(bus.ClientURL())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer serving.Close()
|
|
if err := serving.EnsureControllerBuckets(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := t.Context()
|
|
store, history, err := conditions.OnTheBus(ctx, serving.Conn())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
k := conditions.NewKeeper(ctx, conditions.Options{Store: store, History: history, Looks: storeLooks{inv}})
|
|
defer k.Close(context.Background())
|
|
look := conditions.Observation{Scope: conditions.ScopeMachine, ID: "anchor", Kind: "silent", Machine: "anchor",
|
|
Severity: conditions.Warning, Summary: "anchor has not been heard from", Source: "S1"}
|
|
for range 4 {
|
|
if _, err := k.Observe(ctx, look); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
api, err := jetstream.New(serving.Conn())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stream, err := api.Stream(ctx, "KV_"+broker.ConditionsBucket)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := stream.Info(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.State.LastSeq != 1 {
|
|
t.Fatalf("four looks at one unchanged condition wrote the bucket %d times, want 1", info.State.LastSeq)
|
|
}
|
|
|
|
// The verb as a process other than the serving controller: it reads the bucket and the store.
|
|
keptBefore := conditionsFrom
|
|
conditionsFrom = nil
|
|
defer func() { conditionsFrom = keptBefore }()
|
|
t.Setenv(broker.NATSVar, bus.ClientURL())
|
|
t.Setenv(broker.CertificateVar, "")
|
|
var shown bytes.Buffer
|
|
if err := showCondition(ctx, []string{"machine.anchor.silent", "--json"}, &shown); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var c conditions.Condition
|
|
if err := json.Unmarshal(shown.Bytes(), &c); err != nil {
|
|
t.Fatalf("%v: %s", err, shown.String())
|
|
}
|
|
if c.Observations != 4 || len(c.Evidence) != 4 || c.LastObserved.IsZero() || c.LooksUnread != "" ||
|
|
time.Since(c.LastObserved) > time.Minute {
|
|
t.Fatalf("conditions show reads %d observation(s), %d evidence line(s), last %s (%q)", c.Observations,
|
|
len(c.Evidence), c.LastObserved, c.LooksUnread)
|
|
}
|
|
listed, err := openConditions(ctx)
|
|
if err != nil || len(listed) != 1 || listed[0].Observations != 4 {
|
|
t.Fatalf("conditions lists %+v: %v", listed, err)
|
|
}
|
|
}
|