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:
+10
-10
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user