diff --git a/go/clients/nodeengineclient/enginereads/enginereads.go b/go/clients/nodeengineclient/enginereads/enginereads.go index 6f0f27d..7286477 100644 --- a/go/clients/nodeengineclient/enginereads/enginereads.go +++ b/go/clients/nodeengineclient/enginereads/enginereads.go @@ -143,6 +143,15 @@ type Statement struct { // EventAt is when the part last heard the seat's event manifest-reads-changed for the node, beside when it asked and // what the engine stated then (issue 1966 note 170: was the event heard, and was it asked again after it). EventAt time.Time `json:"event-at,omitzero"` + // Heard is the newest event heard of the node with what it says the engine states, kept so the state shows what the + // rules read while the statement is behind it (issue 1975 note 6), and so a part that starts again holds it. + Heard *HeardEvent `json:"heard,omitempty"` +} + +// HeardEvent is an event manifest-reads-changed as the part kept it: when the engine said it and what it said. +type HeardEvent struct { + At time.Time `json:"at"` + ManifestReads []string `json:"manifest-reads,omitempty"` } // Again is a person's ask again of one node, written by the tool and answered by the part that keeps the state. @@ -473,6 +482,11 @@ func (k *Keeper) take(s Statement) { if at := k.eventAt[s.Node]; at.After(s.EventAt) { s.EventAt = at } + if e, ok := k.said[s.Node]; ok { + if at, err := time.Parse(time.RFC3339Nano, e.At); err == nil { + s.Heard = &HeardEvent{At: at.UTC(), ManifestReads: slices.Clone(e.ManifestReads)} + } + } if !s.Answered { s.UnansweredSince = s.AskedAt if old, ok := k.by[s.Node]; ok && !old.Answered && !old.UnansweredSince.IsZero() { @@ -481,6 +495,7 @@ func (k *Keeper) take(s Statement) { } if old, ok := k.by[s.Node]; ok && !s.Answered && old.From != FromNone { old.Answered, old.AskedAt, old.Words, old.AskAgain, old.UnansweredSince = false, s.AskedAt, s.Words, nil, s.UnansweredSince + old.Heard = s.Heard s = old } k.by[s.Node] = s @@ -529,6 +544,15 @@ func (k *Keeper) Heard(node string, raw []byte, deleted bool) { _, had := k.by[node] if !had { k.by[node] = s + if h := s.Heard; h != nil && !h.At.IsZero() { + if k.said == nil { + k.said, k.catching = map[string]nodeengine.ManifestReadsChangedEvent{}, map[string]int{} + } + if _, ok := k.said[node]; !ok { + k.said[node] = nodeengine.ManifestReadsChangedEvent{V: ContractVersion, Node: node, Why: ReadsStarted, + At: h.At.UTC().Format(time.RFC3339Nano), ManifestReads: slices.Clone(h.ManifestReads)} + } + } } k.mu.Unlock() if !had && k.Changed != nil { diff --git a/go/clients/nodeengineclient/enginereads/enginereads_test.go b/go/clients/nodeengineclient/enginereads/enginereads_test.go index c957e9c..2efe8d6 100644 --- a/go/clients/nodeengineclient/enginereads/enginereads_test.go +++ b/go/clients/nodeengineclient/enginereads/enginereads_test.go @@ -246,6 +246,22 @@ func TestAStatementOlderThanAHeardEventDoesNotStand(t *testing.T) { t.Errorf("after the event's ask: %v, %d calls", s.ManifestReads, calls) } + // The event heard is in the node's statement as the part keeps it, and a part that starts again holds it. + var kept []byte + keeper := &Keeper{Sync: true, Put: func(_ string, v []byte) error { kept = v; return nil }, Call: func(string, string, any) (json.RawMessage, error) { + return answer(`"seat:a"`, early), nil + }} + keeper.HeardChanged(event) + keeper.AskNode("ace", "x") + if st, err := Decode(kept); err != nil || st.Heard == nil || len(st.Heard.ManifestReads) != 2 { + t.Errorf("the kept statement shows the event heard as %+v, %v", st.Heard, err) + } + restarted := &Keeper{Sync: true} + restarted.Heard("ace", kept, false) + if got := restarted.Reads([]string{"ace"})["ace"]; len(got) != 2 { + t.Errorf("a part started again reads %v, want the event's", got) + } + // An engine that answers the old statement to every ask: the event's reads stand, the asks are bounded. calls = 0 stuck := &Keeper{Sync: true, Call: func(string, string, any) (json.RawMessage, error) {