V-1: W1.4: a first view says the revision view it holds, what it waits for and since when
A part with no view waits and says so (design 47, A part with no view says it waits): views' Follower gains WaitFor and Holds, its Status the view held, the one waited for, since when and whether view-waits is written; past WaitsBound (five minutes, a context's bound) RaiseWaits is told once, and holding the view clears it. ViewWaitsFinding names the part, the owner and the revision. The existing <module>_first_views tool answers it; no seat type enters views. Builds: design 47 G §200, §245, §262 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +14,9 @@ const (
|
||||
TooLargeKind = "view-too-large"
|
||||
UnsaidKind = "view-unsaid"
|
||||
UnansweredKind = "first-view-unanswered"
|
||||
// ViewWaitsKind is a part's finding of a view it has waited for past WaitsBound (design 47 *A part with no view says it
|
||||
// waits*).
|
||||
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
|
||||
@@ -73,3 +77,15 @@ func UnansweredFinding(reader string, s Status, at time.Time) Finding {
|
||||
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
|
||||
summary := fmt.Sprintf("%s waits for %s's view %s since %s and acts on nothing new until it holds it; it holds %s",
|
||||
part, s.Owner, 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)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ type Follower struct {
|
||||
// nothing.
|
||||
Raise func(Status)
|
||||
Clear func(Status)
|
||||
// RaiseWaits and ClearWaits are the part's view-waits finding: written past WaitsBound of a WaitFor, cleared when it
|
||||
// Holds the view. Nil says nothing.
|
||||
RaiseWaits func(Status)
|
||||
ClearWaits func(Status)
|
||||
// Changed is told of each key the picture changes (deleted: the key is gone), after the change is held, with when the
|
||||
// owner's bus wrote it: the event's time, or for a key a first view brought the time its answer says (Answer.Written;
|
||||
// zero when the owner does not say it, and for a key the first view no longer holds).
|
||||
@@ -87,6 +91,76 @@ type Status struct {
|
||||
// Why is the last reason it asked (start, a gap and which, a hand act) or why the last ask failed.
|
||||
Why string `json:"why,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// View is the revision of the revision view the part holds from this first view, as its seat's client names it
|
||||
// (design 47 *A part with no view says it waits*); WaitsFor the one it waits for and acts on nothing new until it
|
||||
// holds, since WaitingSince; WaitsRaised says its view-waits finding is written (past WaitsBound).
|
||||
View string `json:"view,omitempty"`
|
||||
WaitsFor string `json:"waits-for,omitempty"`
|
||||
WaitingSince time.Time `json:"waiting-since,omitzero"`
|
||||
WaitsRaised bool `json:"waits-raised,omitempty"`
|
||||
}
|
||||
|
||||
// WaitsBound is how long a part waits for a view before it writes its finding view-waits (design 47): the bound the seat
|
||||
// list's readers keep, so an owner down for a restart raises nothing.
|
||||
const WaitsBound = 5 * time.Minute
|
||||
|
||||
// waitBound waits d within ctx: true when d passed, false when ctx ended first. A variable so a test ends it at once.
|
||||
var waitBound = func(ctx context.Context, d time.Duration) bool {
|
||||
b, cancel := context.WithTimeout(ctx, d)
|
||||
defer cancel()
|
||||
<-b.Done()
|
||||
return ctx.Err() == nil
|
||||
}
|
||||
|
||||
// WaitFor says the part waits for view, a revision of the revision view its seat's client names, and acts on nothing
|
||||
// new until it holds it (Holds): the status says what and since when, and if it still waits for it past WaitsBound,
|
||||
// RaiseWaits is told (the part writes view-waits, ViewWaitsFinding). Waiting for the view it waits for already changes
|
||||
// nothing, not even since when.
|
||||
func (f *Follower) WaitFor(ctx context.Context, view string) {
|
||||
f.mu.Lock()
|
||||
f.init()
|
||||
if f.st.WaitsFor == view || view == "" {
|
||||
f.mu.Unlock()
|
||||
return
|
||||
}
|
||||
f.st.WaitsFor, f.st.WaitingSince, f.st.WaitsRaised = view, f.now(), false
|
||||
s := f.st
|
||||
f.mu.Unlock()
|
||||
f.logf("views: %s waits for %s's view %s, from %s", f.Tool, f.Owner, view, s.WaitingSince.Format(time.RFC3339))
|
||||
f.say(s)
|
||||
go func() {
|
||||
if !waitBound(ctx, WaitsBound) {
|
||||
return
|
||||
}
|
||||
f.mu.Lock()
|
||||
raise := f.st.WaitsFor == view && !f.st.WaitsRaised
|
||||
if raise {
|
||||
f.st.WaitsRaised = true
|
||||
}
|
||||
s := f.st
|
||||
f.mu.Unlock()
|
||||
if raise {
|
||||
f.logf("views: %s has waited for %s's view %s since %s", f.Tool, f.Owner, view, s.WaitingSince.Format(time.RFC3339))
|
||||
if f.RaiseWaits != nil {
|
||||
f.RaiseWaits(s)
|
||||
}
|
||||
f.say(s)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Holds says the part holds view now: it waits no more, and a raised view-waits is cleared (ClearWaits).
|
||||
func (f *Follower) Holds(view string) {
|
||||
f.mu.Lock()
|
||||
f.init()
|
||||
raised := f.st.WaitsRaised
|
||||
f.st.View, f.st.WaitsFor, f.st.WaitingSince, f.st.WaitsRaised = view, "", time.Time{}, false
|
||||
s := f.st
|
||||
f.mu.Unlock()
|
||||
if raised && f.ClearWaits != nil {
|
||||
f.ClearWaits(s)
|
||||
}
|
||||
f.say(s)
|
||||
}
|
||||
|
||||
func (f *Follower) now() time.Time {
|
||||
|
||||
+2
-1
@@ -101,7 +101,8 @@ func StatusTools(reader, read, again string, st stdio.KeptState, acts HandActs,
|
||||
Description: fmt.Sprintf("%s's first views of the owners' states it follows (novox/hq ADR 0615): per owner and state, "+
|
||||
"the tool it asks, the revision its picture holds now (events taken in included), the highest revision it heard and why "+
|
||||
"it is behind it, if it is, when it asked and was answered, how many asks (by hand among them) and whether its "+
|
||||
"condition (unanswered past two minutes) is raised.", reader),
|
||||
"condition (unanswered past two minutes) is raised; and the revision of the revision view it holds, the one it waits "+
|
||||
"for and acts on nothing new until it holds, since when, and whether its view-waits finding (past five minutes) is written.", reader),
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
||||
Run: func(map[string]any) (any, error) {
|
||||
keys, err := st.Keys()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -299,6 +300,60 @@ func TestViews(t *testing.T) {
|
||||
t.Errorf("said %v after %d asks", *said, f.Status().Asks)
|
||||
}
|
||||
})
|
||||
// W1.4 (design 47 *A part with no view says it waits*): the status says the view held, the one waited for and since
|
||||
// when; past the bound the part's view-waits is raised once, and holding the view clears it.
|
||||
t.Run("a part that waits for a view says so, raises view-waits past its bound, and holding it clears that", func(t *testing.T) {
|
||||
ended := make(chan bool)
|
||||
kept := waitBound
|
||||
waitBound = func(context.Context, time.Duration) bool { return <-ended }
|
||||
defer func() { waitBound = kept }()
|
||||
var raised, cleared, said []Status
|
||||
var mu sync.Mutex
|
||||
f := &Follower{Owner: "o", Tool: "o.revision", State: "catalog", Now: now,
|
||||
RaiseWaits: func(s Status) { mu.Lock(); raised = append(raised, s); mu.Unlock() },
|
||||
ClearWaits: func(s Status) { mu.Lock(); cleared = append(cleared, s); mu.Unlock() },
|
||||
Said: func(s Status) { mu.Lock(); said = append(said, s); mu.Unlock() }}
|
||||
f.Holds("trunk.11")
|
||||
f.WaitFor(context.Background(), "trunk.12")
|
||||
f.WaitFor(context.Background(), "trunk.12") // the same view again: nothing new, no second bound
|
||||
if s := f.Status(); s.View != "trunk.11" || s.WaitsFor != "trunk.12" || !s.WaitingSince.Equal(now()) || s.WaitsRaised {
|
||||
t.Fatalf("status = %+v", s)
|
||||
}
|
||||
ended <- true
|
||||
for i := 0; i < 1000 && !f.Status().WaitsRaised; i++ {
|
||||
runtime.Gosched()
|
||||
}
|
||||
mu.Lock()
|
||||
if len(raised) != 1 || raised[0].WaitsFor != "trunk.12" {
|
||||
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" || !strings.Contains(fd.Summary, "waits for o's view trunk.12") ||
|
||||
!strings.Contains(fd.Summary, "it holds trunk.11") {
|
||||
t.Errorf("finding = %+v", fd)
|
||||
}
|
||||
f.Holds("trunk.12")
|
||||
if s := f.Status(); s.View != "trunk.12" || s.WaitsFor != "" || s.WaitsRaised {
|
||||
t.Errorf("after holding: %+v", s)
|
||||
}
|
||||
mu.Lock()
|
||||
if len(cleared) != 1 || len(said) == 0 || said[len(said)-1].View != "trunk.12" {
|
||||
t.Errorf("cleared %d, last said %+v", len(cleared), said)
|
||||
}
|
||||
mu.Unlock()
|
||||
f.WaitFor(context.Background(), "trunk.13")
|
||||
f.Holds("trunk.13")
|
||||
ended <- true // the bound of a view now held raises nothing
|
||||
for i := 0; i < 100; i++ {
|
||||
runtime.Gosched()
|
||||
}
|
||||
mu.Lock()
|
||||
if len(raised) != 1 {
|
||||
t.Errorf("a view held before its bound raised: %+v", raised)
|
||||
}
|
||||
mu.Unlock()
|
||||
})
|
||||
t.Run("an event heard while an ask runs is taken again after its answer", func(t *testing.T) {
|
||||
var f *Follower
|
||||
f, _ = follower(func(ctx context.Context, after string) (Answer, error) {
|
||||
|
||||
Reference in New Issue
Block a user