diff --git a/modules/mesh-conditions/cmd/mesh-conditions/catalog.go b/modules/mesh-conditions/cmd/mesh-conditions/catalog.go index 4b9cb39dc..e8b6b2fcd 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/catalog.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/catalog.go @@ -347,6 +347,7 @@ type keptView struct { Pending map[string]time.Time `json:"pending,omitempty"` Asks map[string]time.Time `json:"asks,omitempty"` Findings []seat.Finding `json:"findings,omitempty"` + Sets map[string]time.Time `json:"sets,omitempty"` } // saveView keeps what the keeper heard of the observer on the node, by compare-and-set; a source is kept once it is heard. @@ -360,7 +361,7 @@ func (k *Keeper) saveView(observer, node string) { } v.kept = true value := keptView{Observer: observer, Node: node, At: v.At, Pending: maps.Clone(v.Pending), Asks: maps.Clone(v.Asks), - Findings: slices.Clone(v.Findings)} + Findings: slices.Clone(v.Findings), Sets: maps.Clone(v.Sets)} k.mu.Unlock() key := viewKey(observer, node) for range ConditionWrites { @@ -403,6 +404,9 @@ func (k *Keeper) RestoreViews() { v.At, v.Findings, v.kept = kv.At, kv.Findings, true maps.Copy(v.Pending, kv.Pending) maps.Copy(v.Asks, kv.Asks) + if len(kv.Sets) > 0 { + v.Sets = maps.Clone(kv.Sets) + } k.mu.Unlock() } } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/events.go b/modules/mesh-conditions/cmd/mesh-conditions/events.go index 458469f9f..4eba71a96 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/events.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/events.go @@ -35,13 +35,35 @@ func emitterOf(key, event string) (string, bool) { return m, ok && m != "" && !strings.Contains(m, ".") } +// subjectOf reads the observer, the event and the node out of an event's key in either form of a part's finding stream +// (design 47 G §529): the old form `mesh.stream..event..`, which the runtime delivers as +// `..` (or as `.` with the node in the envelope), and the template form +// `mesh.bus..finding-events..`. The node of the key is the subject's alone; the envelope's is the +// fallback of a key without one. +func subjectOf(key, envelopeNode string) (module, event, node string, ok bool) { + parts := strings.Split(key, ".") + switch { + case len(parts) == 6 && parts[0] == "mesh" && parts[1] == "bus" && parts[3] == FindingEventsTemplate: + return parts[2], parts[4], parts[5], true + case len(parts) == 3: + return parts[0], parts[1], parts[2], true + case len(parts) == 2: + return parts[0], parts[1], envelopeNode, true + } + return "", "", "", false +} + // FindingEvent is one `finding-written` or `finding-cleared` heard from an observer on a node. func (k *Keeper) FindingEvent(e stdio.Envelope) error { - if observer, ok := emitterOf(e.Key, EventFindingWritten); ok { - return k.FindingWritten(observer, e.Node, e.Body) - } - if observer, ok := emitterOf(e.Key, EventFindingCleared); ok { - return k.FindingCleared(observer, e.Node, e.Body) + if module, event, node, ok := subjectOf(e.Key, e.Node); ok { + switch event { + case EventFindingWritten: + return k.FindingWritten(module, node, e.Body) + case EventFindingCleared: + return k.FindingCleared(module, node, e.Body) + case EventFindingsStated: + return k.FindingsStated(module, node, e.Body) + } } if _, ok := emitterOf(e.Key, watchdogs.EventMachineAway); ok && strings.HasPrefix(e.Key, watchdogs.Seat+".") { return k.MachinePresence(e.Body, true) @@ -55,9 +77,6 @@ func (k *Keeper) FindingEvent(e stdio.Envelope) error { if e.Key == broker.Seat+"."+broker.EventCreated { return k.BrokerCreated(e.Body) } - if observer, ok := emitterOf(e.Key, EventFindingsStated); ok { - return k.FindingsStated(observer, e.Node, e.Body) - } return nil } @@ -244,6 +263,9 @@ type sourceView struct { At time.Time Pending map[string]time.Time Findings []seat.Finding + // Sets are the last findings-stated's at of each set of kinds a statement covers (design 47 G §580), keyed by the kinds + // sorted and joined by a comma; the empty key is a statement with no kinds, which covers every kind. + Sets map[string]time.Time // Asks are the asks it has not answered, with the end of their bound (asks.go). Asks map[string]time.Time // kept says the source is in the state `findings-views` (catalog.go). @@ -313,13 +335,29 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error { k.mu.Lock() k.init() v := k.view(observer, node) - if !v.At.IsZero() && !s.At.After(v.At) { + last := v.Sets[setOf(s.Kinds)] + if len(s.Kinds) == 0 && last.IsZero() { + last = v.At + } + if !last.IsZero() && !s.At.After(last) { k.mu.Unlock() - k.Logf("%s's findings-stated on %s at %s is not newer than the one at %s: left out", observer, node, s.At, v.At) + k.Logf("%s's findings-stated on %s at %s is not newer than the one at %s: left out", observer, node, s.At, last) return nil } - v.At = s.At - v.Findings = s.Findings + covered := func(kind string) bool { return len(s.Kinds) == 0 || slices.Contains(s.Kinds, kind) } + v.At = later(v.At, s.At) + if v.Sets == nil { + v.Sets = map[string]time.Time{} + } + v.Sets[setOf(s.Kinds)] = s.At + // Only the kinds the statement covers are replaced: another bundle's findings and pending keys stand (§580). + kept := v.Findings[:0:0] + for _, f := range v.Findings { + if !covered(f.Kind) { + kept = append(kept, f) + } + } + v.Findings = append(kept, s.Findings...) answered := v.Asks v.Asks = map[string]time.Time{} named := map[string]bool{} @@ -328,6 +366,11 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error { } oldPending := v.Pending v.Pending = map[string]time.Time{} + for key, until := range oldPending { + if !covered(keyKind(key)) { + v.Pending[key] = until + } + } type pend struct { key string deadline time.Time @@ -343,8 +386,8 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error { pending = append(pending, pend{p.Key, s.At.Add(bound)}) } var drop []string - for key := range k.findings[observer] { - if !named[key] && (once || k.keyNode[observer][key] == node) { + for key, f := range k.findings[observer] { + if !named[key] && covered(f.Kind) && (once || k.keyNode[observer][key] == node) { drop = append(drop, key) } } @@ -402,6 +445,28 @@ func (k *Keeper) FindingsStated(observer, node string, body []byte) error { return nil } +// setOf is the key of a set of kinds: the kinds sorted and joined by a comma; none is the empty key, every kind. +func setOf(kinds []string) string { + sorted := slices.Clone(kinds) + sort.Strings(sorted) + return strings.Join(sorted, ",") +} + +// keyKind is the last token of a finding's key, its kind (or its ref, §14 of ADR 0564). +func keyKind(key string) string { + if i := strings.LastIndex(key, "."); i >= 0 { + return key[i+1:] + } + return key +} + +func later(a, b time.Time) time.Time { + if b.After(a) { + return b + } + return a +} + // PendingPassed is a pending key's bound passing: unless a finding or a later set named it since, its condition is cleared. func (k *Keeper) PendingPassed(bound string) error { rest := strings.TrimPrefix(bound, pendingPrefix) diff --git a/modules/mesh-conditions/cmd/mesh-conditions/owner_test.go b/modules/mesh-conditions/cmd/mesh-conditions/owner_test.go index 075866354..4b8326f65 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/owner_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/owner_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + stdio "git.novox.be/novox/mesh-sdk/go" + 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" ) @@ -79,3 +81,63 @@ func TestTheKeeperHandsAFailedHeadToTheOperatorAfterTwoHours(t *testing.T) { t.Fatalf("a restarted keeper, the time past, hands it over from `since`: %+v", c) } } + +// Design 47 G §580, §529: a statement carrying `kinds` clears only the conditions of those kinds the statement does not name, +// so two bundles of one observer on one node keep each other's conditions open; a statement with no kinds covers every +// kind; and the finding events are taken from either form of the part's finding stream. +func TestAStatementCoversOnlyItsKindsAndEventsComeInEitherForm(t *testing.T) { + r := newRig() + t0 := r.now + mk := func(kind string) seat.Finding { + f := eventFinding(watchman, t0) + f.Kind, f.ID, f.Subject.ID = kind, "g14", "g14" + return f + } + stated := func(at time.Time, kinds []string, fs ...seat.Finding) { + t.Helper() + if err := r.k.FindingsStated(watchman, "ace", mustJSON(t, seat.FindingsStated{At: at, Kinds: kinds, Findings: fs})); err != nil { + t.Fatal(err) + } + } + keyOf := func(kind string) string { return cc.Key(cc.ScopeMachine, "g14", kind) } + // Both bundles state their kinds; each raises its own. + stated(t0, []string{"alpha"}, mk("alpha")) + stated(t0.Add(time.Second), []string{"beta"}, mk("beta")) + for _, kind := range []string{"alpha", "beta"} { + if _, ok := r.open(keyOf(kind)); !ok { + t.Fatalf("%s is raised", kind) + } + } + // Alpha's bundle states nothing now: only alpha's condition is cleared, beta's stays open. + stated(t0.Add(2*time.Second), []string{"alpha"}) + if _, ok := r.open(keyOf("alpha")); ok { + t.Error("alpha's condition is cleared by alpha's own statement") + } + if _, ok := r.open(keyOf("beta")); !ok { + t.Error("beta's condition stays open: another bundle's statement does not clear it") + } + // A statement with no kinds covers every kind. + stated(t0.Add(3*time.Second), nil) + if _, ok := r.open(keyOf("beta")); ok { + t.Error("a statement with no kinds clears every kind it does not name") + } + // Either form of the stream's subject reaches the same handler, the node taken from the subject. + body := mustJSON(t, eventFinding(watchman, t0.Add(time.Minute))) + for _, key := range []string{watchman + ".finding-written.ace", "mesh.bus." + watchman + ".finding-events.finding-written.ace"} { + r.k.Settle(keyOf("silent")) + if err := r.k.FindingEvent(stdioEnvelope(key, "", body)); err != nil { + t.Fatal(err) + } + if _, ok := r.open(keyOf("silent")); !ok { + t.Errorf("%s raised nothing", key) + } + if err := r.k.FindingEvent(stdioEnvelope(watchman+".finding-cleared.ace", "", mustJSON(t, seat.FindingCleared{At: t0.Add(2 * time.Minute), + ID: "g14", Kind: "silent", Scope: cc.ScopeMachine, Why: "back"}))); err != nil { + t.Fatal(err) + } + } +} + +func stdioEnvelope(key, node string, body []byte) stdio.Envelope { + return stdio.Envelope{Key: key, Node: node, Body: body} +}