COND10 (stated kinds, both subject forms): the keeper clears only conditions of the kinds a statement covers
A findings-stated carrying kinds sets the keeper's view of that observer, node and set of kinds, and clears only the conditions of those kinds it does not name; no kinds covers every kind. The finding events are taken from either form of the part's finding stream, the observer and node read from the subject. Builds: design 47 G §580, §529 (A-30), §383, §384 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.<module>.event.<event>.<node>`, which the runtime delivers as
|
||||
// `<module>.<event>.<node>` (or as `<module>.<event>` with the node in the envelope), and the template form
|
||||
// `mesh.bus.<module>.finding-events.<event>.<node>`. 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)
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user