G A-22, A-26: views.Finding, FindingWriter and WriterFindings are gone; the readers and the emitter lay out and write their findings through the part's generic writer

The reader's four findings (unanswered, view-waits, unsaid, too-large) are laid out by mesh-sdk (design 47 G §687) on the
wire form and written through views.WireWriter, which the generic Writer is; the emitter writes view-too-large itself and its
TooLarge hook goes; Adopt wires every adopted follower's four callbacks to its reader.

Builds: design 47 G §686, §687, §689, A-22, A-26
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:57:46 +01:00
parent 54c0c3bf22
commit 948cc1b34e
7 changed files with 159 additions and 163 deletions
+10 -10
View File
@@ -24,12 +24,10 @@ type Emitter struct {
Publish func(ctx context.Context, msg *nats.Msg, id string) error
// Marks is the owner's state MarksState; nil says nothing is marked, and every start says from the bucket's end.
Marks jetstream.KeyValue
// TooLarge is told of a value too large for an event (said as a gap): the owner's finding naming the key. Nil
// only logs it.
TooLarge func(state, key string, size int)
// Findings writes and clears the owner's finding of a change not said past UnsaidAfter (UnsaidKind, its own state
// `findings`); nil says it in the log only.
Findings FindingWriter
// Findings is the owner's writer of its findings (its own state `findings`): the emitter writes and clears through it the
// finding of a change not said past UnsaidAfter (UnsaidKind) and writes that of a value too large for an event (TooLargeKind),
// laid out by mesh-sdk (design 47 G §687); nil says them in the log only.
Findings WireWriter
// UnsaidAfter is how long a change may stay not said before it is the owner's finding; zero is Bound.
UnsaidAfter time.Duration
// Wait pauses before a change not said is said again (its attempt so far); it answers false when ctx ended. Nil is
@@ -179,7 +177,7 @@ func (e Emitter) raiseUnsaid(state string, u jetstream.KeyValueEntry, first time
if e.Findings == nil {
return false
}
if err := e.Findings.Put(UnsaidFinding(e.Owner, state, u.Key(), u.Revision(), first, cause.Error(), e.now())); err != nil {
if err := unsaidFinding(e.Owner, state, u.Key(), u.Revision(), first, cause.Error(), e.now()).put(e.Findings); err != nil {
e.logf("views: the finding of %s's change not said could not be written: %v", state, err)
return false
}
@@ -188,7 +186,7 @@ func (e Emitter) raiseUnsaid(state string, u jetstream.KeyValueEntry, first time
// clearUnsaid clears the finding once the change is said; it answers whether it is cleared.
func (e Emitter) clearUnsaid(state string) bool {
if err := e.Findings.Delete(UnsaidFinding(e.Owner, state, "", 0, time.Time{}, "", e.now())); err != nil {
if err := unsaidFinding(e.Owner, state, "", 0, time.Time{}, "", e.now()).drop(e.Findings); err != nil {
e.logf("views: the finding of %s's change not said could not be cleared: %v", state, err)
return false
}
@@ -226,8 +224,10 @@ func (e Emitter) Event(state string, made time.Time, u jetstream.KeyValueEntry)
if n := len(u.Value()); n > MaxValueBytes {
ev.Gap = true
e.logf("views: %s.%s at %d holds %d bytes, more than an event carries: said as a gap, so a reader asks its first view", state, u.Key(), u.Revision(), n)
if e.TooLarge != nil {
e.TooLarge(state, u.Key(), n)
if e.Findings != nil {
if err := tooLargeFinding(e.Owner, state, u.Key(), n, e.now()).put(e.Findings); err != nil {
e.logf("views: the finding of %s.%s too large could not be written: %v", state, u.Key(), err)
}
}
return ev
}
+11
View File
@@ -0,0 +1,11 @@
package views
// PutViewFinding and DropViewFinding write and clear one of a views reader's findings through w, laid out as mesh-sdk lays
// the reader's own (design 47 G §687), for the tests of package views_test.
func PutViewFinding(w WireWriter, kind, id, summary, headline string) error {
return viewFinding{kind: kind, id: id, summary: summary, headline: headline}.put(w)
}
func DropViewFinding(w WireWriter, kind, id string) error {
return viewFinding{kind: kind, id: id}.drop(w)
}
+62 -53
View File
@@ -2,9 +2,12 @@ package views
import (
"cmp"
"context"
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The findings a views part writes in its own state `findings`, which the condition keeper raises and clears
@@ -19,74 +22,80 @@ const (
ViewWaitsKind = "view-waits"
)
// Finding is what a views part says is wrong with a state it owns or follows, in plain fields: Kind and ID name it (a module-scoped
// finding of the observer), Summary, Headline and Explanation say it. It carries no layout: the writer a part gives
// (FindingWriter) encodes it as the condition keeper reads it, which is the seat mesh-conditions' contract, not this
// package's (novox/hq ADR 0634 §4, ADR 0635 §4).
type Finding struct {
Observer string
Kind string
ID string
Summary string
Headline string
Explanation string
At time.Time
// viewFinding is what a views part says is wrong with a state it owns or follows, in plain fields: kind and id name it, summary
// and headline say it. It is laid out in the wire form of the contract's finding by mesh-sdk (body, design 47 G §687, A-22) and
// written through the part's generic writer; there is no other form of it.
type viewFinding struct {
kind string
id string
summary string
headline string
at time.Time
}
// FindingWriter writes and clears a part's findings in its own state `findings` (WriterFindings: through the part's mesh-sdk
// writer, design 47 G §639), where the condition keeper raises and
// clears conditions from them (novox/hq ADR 0564 §2): the part gives the one that encodes a Finding as that state's layout.
type FindingWriter interface {
Put(f Finding) error
Delete(f Finding) error
// WireWriter is what a views part's findings use of the part's finding writer, whatever the contract's body types: the
// generic Writer[F, C, S, A] is one (design 47 G §639, §687, A-22).
type WireWriter interface {
WriteWire(ctx context.Context, finding map[string]any) error
Clear(ctx context.Context, scope, id, kind, why string) error
ModuleNode() (module, node string)
}
// UnsaidFinding is the owner's finding of a change of state it has not said on its views family since since: every
// reader of the state stands still at the change before it until it is said, which clears this.
func UnsaidFinding(owner, state, key string, revision uint64, since time.Time, cause string, at time.Time) Finding {
id := owner + "." + state
// FindingScope is the scope of a views reader's findings: they are findings of the part's module (design 47 G §687).
const FindingScope = "module"
// body lays the finding out on its wire form (design 47 G §687): `kind` the finding's, `scope` module, `id` the finding's id
// escaped as a key's name is, `severity` warning but urgent for view-too-large, `source` views, `subject` the part's module on
// its node, `summary` and `headline` the finding's texts.
func (f viewFinding) body(module, node string) map[string]any {
sev := "warning"
if f.kind == TooLargeKind {
sev = "urgent"
}
subject := map[string]any{"scope": FindingScope, "id": module}
if node != "" {
subject["machine"] = node
}
b := map[string]any{"kind": f.kind, "scope": FindingScope, "id": keys.EscapeName(f.id), "severity": sev, "source": "views",
"subject": subject, "summary": f.summary}
if f.headline != "" {
b["headline"] = f.headline
}
if !f.at.IsZero() {
b["at"] = f.at
}
return b
}
// put writes the finding through w; drop clears it.
func (f viewFinding) put(w WireWriter) error {
module, node := w.ModuleNode()
return w.WriteWire(context.Background(), f.body(module, node))
}
func (f viewFinding) drop(w WireWriter) error {
return w.Clear(context.Background(), FindingScope, keys.EscapeName(f.id), f.kind, "the part no longer observes it")
}
func unsaidFinding(owner, state, key string, revision uint64, since time.Time, cause string, at time.Time) viewFinding {
summary := fmt.Sprintf("%s has not said the change of %s.%s at %d on its views family since %s (%s): its readers stand still before it",
owner, state, key, revision, since.UTC().Format(time.RFC3339), strings.TrimSpace(cause))
return finding(owner, UnsaidKind, id, summary,
fmt.Sprintf("%s cannot say a change of its %s", owner, state),
"The owner says each change of its state as an event on its own stream, and says a change again until the bus takes it; "+
"its readers hear nothing newer meanwhile. Look at the owner's stream on the bus: is it full, or is the bus away?", at)
return viewFinding{kind: UnsaidKind, id: owner + "." + state, summary: summary, headline: fmt.Sprintf("%s cannot say a change of its %s", owner, state), at: at.UTC()}
}
// TooLargeFinding is the owner's finding of state's key too large to say whole on its views family.
func TooLargeFinding(owner, state, key string, size int, at time.Time) Finding {
id := owner + "." + state + "." + key
func tooLargeFinding(owner, state, key string, size int, at time.Time) viewFinding {
summary := fmt.Sprintf("%s's %s.%s holds %d bytes, more than one views event carries (%d): every reader asks its first view again at each change of it", owner, state, key, size, MaxValueBytes)
return finding(owner, TooLargeKind, id, summary,
fmt.Sprintf("A value of %s is too large for its views event", owner),
"Each change of this key is said as a gap, so every reader of the state asks the owner's first view once more. "+
"It works, but costs a first view per change; the value should be made smaller or split.", at)
return viewFinding{kind: TooLargeKind, id: owner + "." + state + "." + key, summary: summary, headline: fmt.Sprintf("A value of %s is too large for its views event", owner), at: at.UTC()}
}
// UnansweredFinding is a reader's finding of its first view of s unanswered past its bound (ADR 0615 §6): it names the
// owner and the tool. A first view answered at revision 0 with nothing in it is said too, with the operator's step.
func UnansweredFinding(reader string, s Status, at time.Time) Finding {
id := reader + "." + s.Owner + "." + s.State
func unansweredFinding(reader string, s Status, at time.Time) viewFinding {
summary := fmt.Sprintf("%s has no first view of %s's %s: %s has not answered since %s (%s)", reader, s.Owner, s.State, s.Tool,
s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error))
return finding(reader, UnansweredKind, id, summary,
fmt.Sprintf("%s waits for %s", reader, s.Owner),
fmt.Sprintf("%s reads %s's %s only through %s, and keeps asking it; it never reads the state itself. The answer clears this. "+
"Look at %s's runtime of its own: is it running and connected?", reader, s.Owner, s.State, s.Tool, s.Owner), at)
return viewFinding{kind: UnansweredKind, id: reader + "." + s.Owner + "." + s.State, summary: summary, headline: fmt.Sprintf("%s waits for %s", reader, s.Owner), at: at.UTC()}
}
func finding(observer, kind, id, summary, headline, explanation string, at time.Time) Finding {
return Finding{Observer: observer, Kind: kind, ID: id, Summary: summary, Headline: headline, Explanation: explanation, At: at.UTC()}
}
// ViewWaitsFinding is a part's finding of the view it has waited for since s.WaitingSince (design 47): it names the
// part, the owner and the revision, and clears when the part holds it.
func ViewWaitsFinding(part string, s Status, at time.Time) Finding {
id := part + "." + s.Owner + "." + s.State
func viewWaitsFinding(part string, s Status, at time.Time) viewFinding {
summary := fmt.Sprintf("%s waits for the view %s.%s at revision %s since %s and acts on nothing new until it holds it; it holds %s",
part, s.Owner, s.State, s.WaitsFor, s.WaitingSince.UTC().Format(time.RFC3339), cmp.Or(s.View, "none"))
return finding(part, ViewWaitsKind, id, summary,
fmt.Sprintf("%s waits for a view of %s", part, s.Owner),
fmt.Sprintf("%s keeps running what it runs and takes up nothing new until %s's view %s reaches it. Look at %s: is it running, and has it "+
"said that revision? %s's first views tool says what it holds and waits for.", part, s.Owner, s.WaitsFor, s.Owner, part), at)
return viewFinding{kind: ViewWaitsKind, id: part + "." + s.Owner + "." + s.State, summary: summary, headline: fmt.Sprintf("%s waits for a view of %s", part, s.Owner), at: at.UTC()}
}
+7 -7
View File
@@ -36,7 +36,7 @@ type ProcessReader struct {
Call func(key string, body any) (json.RawMessage, error)
// Findings is the reader's own state `findings`, where the condition keeper raises and clears the condition of a
// first view unanswered past its bound (ADR 0615 §6); nil says it in the log only.
Findings func() FindingWriter
Findings func() WireWriter
Logf func(string, ...any)
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
Now func() time.Time
@@ -250,12 +250,12 @@ func (r *ProcessReader) waits(s Status, raised bool) {
if r.Now != nil {
at = r.Now()
}
f := ViewWaitsFinding(r.Module, s, at)
f := viewWaitsFinding(r.Module, s, at)
var err error
if raised {
err = kv.Put(f)
err = f.put(kv)
} else {
err = kv.Delete(f)
err = f.drop(kv)
}
if err != nil {
r.logf("views: the view-waits finding of %s is not written: %v", s.Tool, err)
@@ -306,12 +306,12 @@ func (r *ProcessReader) finding(s Status, raised bool) {
if r.Now != nil {
at = r.Now()
}
f := UnansweredFinding(r.Module, s, at)
f := unansweredFinding(r.Module, s, at)
var err error
if raised {
err = kv.Put(f)
err = f.put(kv)
} else {
err = kv.Delete(f)
err = f.drop(kv)
}
if err != nil {
r.logf("views: the finding of the first view %s is not written: %v", s.Tool, err)
+6 -58
View File
@@ -26,7 +26,7 @@ type Reader struct {
Module string
// Findings writes and clears this module's finding of an unanswered first view (its own state `findings`); nil
// says it in the log only.
Findings FindingWriter
Findings WireWriter
Logf func(string, ...any)
// Changed is told of each key a followed state changes, after the picture holds it (state is `<owner>.<state>`).
Changed func(state, key string, value json.RawMessage, deleted bool)
@@ -225,12 +225,12 @@ func (r *Reader) waits(s Status, raised bool) {
if r.Findings == nil {
return
}
f := ViewWaitsFinding(r.Module, s, r.nowTime())
f := viewWaitsFinding(r.Module, s, r.nowTime())
var err error
if raised {
err = r.Findings.Put(f)
err = f.put(r.Findings)
} else {
err = r.Findings.Delete(f)
err = f.drop(r.Findings)
}
if err != nil {
r.logf("the view-waits finding of %s is not written: %v", s.Tool, err)
@@ -249,7 +249,7 @@ func (r *Reader) raise(s Status) {
if r.Findings == nil {
return
}
if err := r.Findings.Put(UnansweredFinding(r.Module, s, r.nowTime())); err != nil {
if err := unansweredFinding(r.Module, s, r.nowTime()).put(r.Findings); err != nil {
r.logf("the finding of the unanswered first view %s could not be written: %v", s.Tool, err)
}
}
@@ -258,7 +258,7 @@ func (r *Reader) clear(s Status) {
if r.Findings == nil {
return
}
if err := r.Findings.Delete(UnansweredFinding(r.Module, s, r.nowTime())); err != nil {
if err := unansweredFinding(r.Module, s, r.nowTime()).drop(r.Findings); err != nil {
r.logf("the finding of the first view %s could not be cleared: %v", s.Tool, err)
}
}
@@ -615,55 +615,3 @@ func (r *Reader) Tools(prefix string) []stdio.Tool {
}},
}
}
// FindingScope is the scope of a views reader's findings: they are findings of the part's module (design 47 G §687).
const FindingScope = "module"
// wireWriter is the part of a Writer[F, C, S, A] a views reader's findings use, whatever the contract's body types.
type wireWriter interface {
WriteWire(ctx context.Context, finding map[string]any) error
Clear(ctx context.Context, scope, id, kind, why string) error
ModuleNode() (module, node string)
}
// WriterFindings writes a views reader's findings through the part's mesh-sdk writer, as kinds of the reader's own
// (design 47 G §639, §580, §687): the Findings of a reader, never a writer of its own. mesh-sdk lays each finding out on
// its wire form: `kind` the finding's, `scope` module, `id` the finding's id escaped as a key's name is, `severity`
// warning but urgent for view-too-large, `source` views, `subject` the part's module on its node, `summary` and
// `headline` the finding's texts. The writer's statement covers the reader's open findings beside its observe's. Writer
// is the part's *Writer[F, C, S, A].
type WriterFindings struct {
Writer wireWriter
}
func (o WriterFindings) body(f Finding) map[string]any {
module, node := o.Writer.ModuleNode()
sev := "warning"
if f.Kind == TooLargeKind {
sev = "urgent"
}
subject := map[string]any{"scope": FindingScope, "id": module}
if node != "" {
subject["machine"] = node
}
b := map[string]any{"kind": f.Kind, "scope": FindingScope, "id": keys.EscapeName(f.ID), "severity": sev, "source": "views",
"subject": subject, "summary": f.Summary}
if f.Headline != "" {
b["headline"] = f.Headline
}
if !f.At.IsZero() {
b["at"] = f.At
}
return b
}
// Put writes one finding.
func (o WriterFindings) Put(f Finding) error {
return o.Writer.WriteWire(context.Background(), o.body(f))
}
// Delete clears one finding.
func (o WriterFindings) Delete(f Finding) error {
b := o.body(f)
return o.Writer.Clear(context.Background(), FindingScope, b["id"].(string), f.Kind, "the part no longer observes it")
}
+60 -30
View File
@@ -17,6 +17,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -332,9 +333,9 @@ func TestViews(t *testing.T) {
t.Errorf("raised = %+v", raised)
}
mu.Unlock()
fd := ViewWaitsFinding("mesh-delivery-builder", f.Status(), now())
if fd.Kind != ViewWaitsKind || fd.ID != "mesh-delivery-builder.o.catalog.trunk" || !strings.Contains(fd.Summary, "waits for the view o.catalog.trunk at revision trunk.12") ||
!strings.Contains(fd.Summary, "it holds trunk.11") {
fd := viewWaitsFinding("mesh-delivery-builder", f.Status(), now())
if fd.kind != ViewWaitsKind || fd.id != "mesh-delivery-builder.o.catalog.trunk" || !strings.Contains(fd.summary, "waits for the view o.catalog.trunk at revision trunk.12") ||
!strings.Contains(fd.summary, "it holds trunk.11") {
t.Errorf("finding = %+v", fd)
}
none := &Follower{Owner: "o", Tool: "o.revision", State: BranchState("catalog", "new"), Now: now,
@@ -688,7 +689,7 @@ func TestViews(t *testing.T) {
kept := waitBound
waitBound = func(context.Context, time.Duration) bool { return <-ended }
defer func() { waitBound = kept }()
fs := &signalFindings{put: make(chan Finding, 4)}
fs := &signalFindings{put: make(chan seenFinding, 4)}
r := &Reader{Module: "reader", Findings: fs, Now: now}
var own []string
f := &Follower{Owner: "s", Tool: "seat:s.revision", State: "revisions", Ask: answer,
@@ -729,8 +730,8 @@ func TestViews(t *testing.T) {
}
// The process form: the picture reads the adopted follower, and the seat's event on the durable goes to the client.
pfs := &signalFindings{put: make(chan Finding, 4)}
pr := &ProcessReader{Module: "reader", Findings: func() FindingWriter { return pfs }, Now: now}
pfs := &signalFindings{put: make(chan seenFinding, 4)}
pr := &ProcessReader{Module: "reader", Findings: func() WireWriter { return pfs }, Now: now}
pf := &Follower{Owner: "s", Tool: "seat:s.revision", State: "revisions", Ask: answer}
p, err := pr.Adopt(pf, "s", "revision-said", hear)
if err != nil || p.Bucket() != "s_revisions" || len(pr.Followers()) != 1 {
@@ -818,7 +819,7 @@ func TestViews(t *testing.T) {
heard[branch] = got
return func(_ context.Context, d []byte) error { *got = append(*got, string(d)); return nil }
}
fs := &signalFindings{put: make(chan Finding, 4)}
fs := &signalFindings{put: make(chan seenFinding, 4)}
r := &Reader{Module: "reader", Findings: fs, Now: now}
main := &Follower{Owner: "s", Tool: "seat:s.revision", State: BranchState("revisions", "main"), Ask: answer}
if err := r.Adopt(main, "s", "revision-said", hearer("main")); err != nil {
@@ -863,8 +864,8 @@ func TestViews(t *testing.T) {
}
// The process form: a view adopted after the start on another event is handled on the module's hub.
pfs := &signalFindings{put: make(chan Finding, 4)}
pr := &ProcessReader{Module: "reader", Findings: func() FindingWriter { return pfs }, Now: now, Hub: process.NewEventHub(t.Logf)}
pfs := &signalFindings{put: make(chan seenFinding, 4)}
pr := &ProcessReader{Module: "reader", Findings: func() WireWriter { return pfs }, Now: now, Hub: process.NewEventHub(t.Logf)}
pm := &Follower{Owner: "s", Tool: "seat:s.revision", State: BranchState("revisions", "main"), Ask: answer}
if _, err := pr.Adopt(pm, "s", "revision-said", hearer("pmain")); err != nil {
t.Fatal(err)
@@ -985,16 +986,16 @@ func TestViews(t *testing.T) {
t.Errorf("%d failures: %d findings written, %d cleared, %d left, said %v", c.fails, fs.puts, fs.deletes, len(fs.m), said)
}
if c.puts > 0 {
f := UnsaidFinding("o", "s", "a", 1, made, "bus away", clock)
if f.Kind != UnsaidKind || f.ID != "o.s" || f.Observer != "o" || !strings.Contains(f.Summary, "o has not said the change of s.a at 1") {
f := unsaidFinding("o", "s", "a", 1, made, "bus away", clock)
if f.kind != UnsaidKind || f.id != "o.s" || !strings.Contains(f.summary, "o has not said the change of s.a at 1") {
t.Errorf("the finding is %+v", f)
}
}
}
// A finding a run before a restart raised is cleared by the first change said after it.
left0 := UnsaidFinding("o", "s", "a", 1, made, "bus away", made)
key := left0.Kind + "/" + left0.ID
fs := &findingsMap{m: map[string]Finding{key: left0}}
left0 := unsaidFinding("o", "s", "a", 1, made, "bus away", made)
key := left0.kind + "/" + left0.id
fs := &findingsMap{m: map[string]seenFinding{key: {Kind: left0.kind, ID: left0.id}}}
said, fails = nil, 0
em.Findings = fs
updates = make(chan jetstream.KeyValueEntry, 1)
@@ -1004,6 +1005,12 @@ func TestViews(t *testing.T) {
if _, left := fs.m[key]; left || len(said) != 1 {
t.Errorf("after a restart the leftover finding stands (%v), said %v", left, said)
}
// A value too large for an event is said as a gap, and the emitter writes the owner's view-too-large itself (design 47 G §687).
tl := &findingsMap{}
te := Emitter{Owner: "o", Findings: tl, Now: func() time.Time { return made }}
if ev := te.Event("s", made, kvEntry{key: "big", rev: 3, at: made, size: MaxValueBytes + 1}); !ev.Gap || tl.m[TooLargeKind+"/o.s.big"].Kind != TooLargeKind {
t.Errorf("a value too large: gap %v, findings %v", ev.Gap, tl.m)
}
})
t.Run("each change names the revision said before it, and the mark is said as an event once written", func(t *testing.T) {
var said []Event
@@ -1048,21 +1055,35 @@ func TestViews(t *testing.T) {
})
}
// findingsMap is a module's own findings state in memory, keyed `<kind>/<id>`.
// seenFinding is a finding a fake writer was handed, as its wire form names it.
type seenFinding struct{ Kind, ID, Summary string }
// findingsMap is a module's own findings writer in memory, keyed `<kind>/<id>` (the id as the finding names it).
type findingsMap struct {
m map[string]Finding
m map[string]seenFinding
puts, deletes int
}
func (f *findingsMap) Put(v Finding) error {
func (f *findingsMap) ModuleNode() (string, string) { return "m", "n1" }
func (f *findingsMap) WriteWire(_ context.Context, w map[string]any) error {
id, _ := keys.UnescapeName(w["id"].(string))
v := seenFinding{Kind: w["kind"].(string), ID: id, Summary: w["summary"].(string)}
if f.m == nil {
f.m = map[string]Finding{}
f.m = map[string]seenFinding{}
}
f.m[v.Kind+"/"+v.ID] = v
f.puts++
return nil
}
func (f *findingsMap) Clear(_ context.Context, _, id, kind, _ string) error {
plain, _ := keys.UnescapeName(id)
delete(f.m, kind+"/"+plain)
f.deletes++
return nil
}
// marksKV is an owner's marks state that takes puts and nothing else.
type marksKV struct {
jetstream.KeyValue
@@ -1074,25 +1095,28 @@ func (m *marksKV) Put(context.Context, string, []byte) (uint64, error) {
return uint64(m.puts), nil
}
func (f *findingsMap) Delete(v Finding) error { delete(f.m, v.Kind+"/"+v.ID); f.deletes++; return nil }
// signalFindings is a findingsMap that hands each finding written to put, once written.
type signalFindings struct {
findingsMap
put chan Finding
put chan seenFinding
}
func (f *signalFindings) Put(v Finding) error { _ = f.findingsMap.Put(v); f.put <- v; return nil }
func (f *signalFindings) WriteWire(ctx context.Context, w map[string]any) error {
_ = f.findingsMap.WriteWire(ctx, w)
id, _ := keys.UnescapeName(w["id"].(string))
f.put <- seenFinding{Kind: w["kind"].(string), ID: id, Summary: w["summary"].(string)}
return nil
}
// next is the next finding written; none within a second fails the test (a bound, never waited out when it comes).
func (f *signalFindings) next(t *testing.T) Finding {
func (f *signalFindings) next(t *testing.T) seenFinding {
t.Helper()
select {
case v := <-f.put:
return v
case <-time.After(time.Second):
t.Fatal("no finding was written")
return Finding{}
return seenFinding{}
}
}
@@ -1125,14 +1149,20 @@ func (f actsFunc) RecordHandAct(a HandAct) error { return f(a) }
// kvEntry is one change of a bucket as a watch hands it.
type kvEntry struct {
key string
rev uint64
at time.Time
key string
rev uint64
at time.Time
size int // the value's length when over the one event's: zero is a small value
}
func (k kvEntry) Bucket() string { return "b" }
func (k kvEntry) Key() string { return k.key }
func (k kvEntry) Value() []byte { return []byte(`{"n":1}`) }
func (k kvEntry) Bucket() string { return "b" }
func (k kvEntry) Key() string { return k.key }
func (k kvEntry) Value() []byte {
if k.size > 0 {
return make([]byte, k.size)
}
return []byte(`{"n":1}`)
}
func (k kvEntry) Revision() uint64 { return k.rev }
func (k kvEntry) Created() time.Time { return k.at }
func (k kvEntry) Delta() uint64 { return 0 }
+3 -5
View File
@@ -354,8 +354,7 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
}
}
// a views reader writes through the part's writer, and the statement keeps its open findings
rf := views.WriterFindings{Writer: r.w}
if err := rf.Put(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s", Summary: "waits"}); err != nil {
if err := views.PutViewFinding(r.w, views.ViewWaitsKind, "m.o.s", "waits", ""); err != nil {
t.Fatal(err)
}
r.observe = func(context.Context) (testObserved, error) {
@@ -382,7 +381,7 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
if !slices.Contains(stated.Kinds, "k") || !slices.Contains(stated.Kinds, views.ViewWaitsKind) || len(stated.Findings) != 2 {
t.Errorf("the statement: kinds %v, %d findings", stated.Kinds, len(stated.Findings))
}
if err := rf.Delete(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s"}); err != nil {
if err := views.DropViewFinding(r.w, views.ViewWaitsKind, "m.o.s"); err != nil {
t.Fatal(err)
}
if _, ok := r.state.m["module.m=2eo=2es.view-waits"]; ok {
@@ -396,9 +395,8 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
// (§689).
func TestReaderFindingsAreLaidOutAndCheckFindingRefuses(t *testing.T) {
r := newRig(t)
rf := views.WriterFindings{Writer: r.w}
for _, kind := range views.ViewKinds {
if err := rf.Put(views.Finding{Kind: kind, ID: "o.s.main", Summary: "sum " + kind, Headline: "head"}); err != nil {
if err := views.PutViewFinding(r.w, kind, "o.s.main", "sum "+kind, "head"); err != nil {
t.Fatal(err)
}
e, _ := r.state.Get("module.o=2es=2emain." + kind)