Merge branch 'feat/47-G-fix11-a3' into feat/47-G-join11
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
package firstview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/approval"
|
||||
)
|
||||
|
||||
// Findings is the runner's held finding writer, the one the gate writes its findings through too (design 46 7-approval-mark
|
||||
// §4, §16, §19); nil when it was not made.
|
||||
func (v *Views) Findings() approval.FindingWriter {
|
||||
if v.deps.Findings == nil {
|
||||
return nil
|
||||
}
|
||||
return v.deps.Findings
|
||||
}
|
||||
|
||||
// ApprovalCatalog is the catalog half as the gate reads its marks (design 46 7-approval-mark §5): the entries the branch
|
||||
// follower holds, an entry by fingerprint asked of `mesh-catalog.entry` when no branch lists it, and the seat definitions
|
||||
// the seat list holds (defs).
|
||||
func (v *Views) ApprovalCatalog(defs func() map[string]seatdef.Definition) approval.Catalog {
|
||||
return approvalCatalog{v: v, defs: defs}
|
||||
}
|
||||
|
||||
type approvalCatalog struct {
|
||||
v *Views
|
||||
defs func() map[string]seatdef.Definition
|
||||
}
|
||||
|
||||
func (c approvalCatalog) Ready() error {
|
||||
if c.v.branches == nil {
|
||||
return errNotFollowed
|
||||
}
|
||||
return c.v.branches.Ready()
|
||||
}
|
||||
|
||||
func (c approvalCatalog) Revision() string {
|
||||
if c.v.branches == nil {
|
||||
return ""
|
||||
}
|
||||
for _, branch := range c.v.branches.Followed() {
|
||||
if cat, ok := c.v.branches.Catalog(branch); ok {
|
||||
if half, ok := cat.View(); ok {
|
||||
return strconv.FormatInt(half.Revision.Revision, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c approvalCatalog) Served(fingerprint string) (catalogseat.CatalogEntry, bool, string) {
|
||||
if c.v.branches == nil {
|
||||
return catalogseat.CatalogEntry{}, false, errNotFollowed.Error()
|
||||
}
|
||||
e, state := c.v.branches.Resolve(context.Background(), fingerprint)
|
||||
return e, state == meshcatalogclient.EntryAnswered, string(state)
|
||||
}
|
||||
|
||||
func (c approvalCatalog) Newest(module string) (catalogseat.CatalogEntry, bool) {
|
||||
if c.v.branches == nil {
|
||||
return catalogseat.CatalogEntry{}, false
|
||||
}
|
||||
e, ok, err := c.v.branches.Entry(module)
|
||||
return e, ok && err == nil
|
||||
}
|
||||
|
||||
func (c approvalCatalog) Definitions() map[string]seatdef.Definition {
|
||||
if c.defs == nil {
|
||||
return nil
|
||||
}
|
||||
return c.defs()
|
||||
}
|
||||
|
||||
func (c approvalCatalog) Entries() []catalogseat.CatalogEntry {
|
||||
halves, err := c.v.Halves()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []catalogseat.CatalogEntry
|
||||
for _, half := range halves {
|
||||
for _, e := range half.Entries {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type notFollowed struct{}
|
||||
|
||||
func (notFollowed) Error() string { return "the catalog half is not followed by this runner" }
|
||||
|
||||
var errNotFollowed error = notFollowed{}
|
||||
|
||||
// heardOne wakes every wait on the picture.
|
||||
func (v *Views) heardOne() {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if v.heard != nil {
|
||||
close(v.heard)
|
||||
}
|
||||
v.heard = make(chan struct{})
|
||||
}
|
||||
|
||||
// heardNext is closed at the next event heard.
|
||||
func (v *Views) heardNext() <-chan struct{} {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if v.heard == nil {
|
||||
v.heard = make(chan struct{})
|
||||
}
|
||||
return v.heard
|
||||
}
|
||||
|
||||
// WaitCatalog waits within the bound for the catalog half's first view of every watched branch (design 46 7-approval-mark
|
||||
// §19), waking at each event the runner hears and at each first view answered; nil once it is read, else why not.
|
||||
func (v *Views) WaitCatalog(within time.Duration) error {
|
||||
c := approvalCatalog{v: v}
|
||||
timer := time.NewTimer(within)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
next := v.heardNext()
|
||||
err := c.Ready()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var ready []<-chan struct{}
|
||||
if v.branches != nil {
|
||||
for _, branch := range v.branches.Followed() {
|
||||
if cat, ok := v.branches.Catalog(branch); ok {
|
||||
select {
|
||||
case <-cat.Ready(): // read already: waits on nothing
|
||||
default:
|
||||
ready = append(ready, cat.Ready())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !waitAny(next, timer.C, ready) {
|
||||
return c.Ready()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// waitAny waits for next or one of ready, true, or for the bound, false.
|
||||
func waitAny(next <-chan struct{}, bound <-chan time.Time, ready []<-chan struct{}) bool {
|
||||
woke := make(chan struct{}, 1)
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
for _, r := range ready {
|
||||
go func(r <-chan struct{}) {
|
||||
select {
|
||||
case <-r:
|
||||
woke <- struct{}{}
|
||||
case <-stop:
|
||||
}
|
||||
}(r)
|
||||
}
|
||||
select {
|
||||
case <-next:
|
||||
case <-woke:
|
||||
case <-bound:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// OnSeatHolder calls f whenever the assigner's seat-holders view changes for the seat (lead answer A-157: the gate asks
|
||||
// mesh-hand-acts.warranted again when the hand-act seat's holder is heard again).
|
||||
func (v *Views) OnSeatHolder(seat string, f func()) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if v.onKey == nil {
|
||||
v.onKey = map[string][]func(){}
|
||||
}
|
||||
k := "seat-holders " + seat
|
||||
v.onKey[k] = append(v.onKey[k], f)
|
||||
}
|
||||
|
||||
// heardKey calls the hearers of a key of an owner's state just changed: a seat's holders, under the seat's own key and
|
||||
// its `<seat>.<node>` keys.
|
||||
func (v *Views) heardKey(state, key string) {
|
||||
seat, _, _ := strings.Cut(key, ".")
|
||||
v.mu.Lock()
|
||||
fs := append([]func(){}, v.onKey[state+" "+seat]...)
|
||||
v.mu.Unlock()
|
||||
for _, f := range fs {
|
||||
go f()
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/approval"
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
)
|
||||
|
||||
@@ -66,6 +67,10 @@ type Views struct {
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
deps Deps
|
||||
// heard is closed and made again at every event heard, so a wait for the picture wakes on events, never on a timer.
|
||||
heard chan struct{}
|
||||
// onKey are the hearers of one key of an owner's state, by `<state> <key>` (OnSeatHolder).
|
||||
onKey map[string][]func()
|
||||
}
|
||||
|
||||
// FindingsState is the runner's own state the findings are written in.
|
||||
@@ -120,7 +125,7 @@ func DepsOf(conn *bus.Conn, logf func(string, ...any)) Deps {
|
||||
}
|
||||
w, err := views.NewHeldWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{
|
||||
Asked: condseat.SubjectFindingsAsked, Module: me, Node: conn.Node(), State: findingsState{conn: conn, module: me}, JS: js, Logf: logf,
|
||||
Kinds: []string{views.UnansweredKind}})
|
||||
Kinds: append([]string{views.UnansweredKind}, approval.Kinds...)})
|
||||
if err != nil {
|
||||
logf("[mesh-tools] the runner's findings are not written (%v): the first views' findings are in the log only", err)
|
||||
return d
|
||||
@@ -204,8 +209,10 @@ func StartWith(parent context.Context, d Deps, logf func(string, ...any)) (*View
|
||||
return v, func() { stop(); cancel(); v.wg.Wait(); stopFindings() }
|
||||
}
|
||||
|
||||
// hear applies one views event; a gap asks the first view once more.
|
||||
// hear applies one views event; a gap asks the first view once more. Every event heard wakes what waits on the picture
|
||||
// (heardOne).
|
||||
func (v *Views) hear(env bus.Envelope) error {
|
||||
defer v.heardOne()
|
||||
v.mu.Lock()
|
||||
hs := append([]*adoption(nil), v.hearers[env.Key]...)
|
||||
v.mu.Unlock()
|
||||
@@ -225,6 +232,7 @@ func (v *Views) hear(env bus.Envelope) error {
|
||||
if f == nil || f.Owner != ev.Owner {
|
||||
return nil
|
||||
}
|
||||
defer v.heardKey(ev.State, ev.Key)
|
||||
if gap, why := f.Apply(ev); gap {
|
||||
v.logf("[mesh-tools] a gap in %s's %s (%s): its first view %s is asked once more", f.Owner, f.State, why, f.Tool)
|
||||
go func() { _ = f.Again(v.ctx, "a gap: "+why) }()
|
||||
@@ -437,6 +445,25 @@ func (w wiring) Drop(owner, state string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Halves is the catalog half of each followed branch, by branch: its revision, with the seat definitions it was judged
|
||||
// with, and its entries; mesh_wait checks an event name's emitter against them (design 34 every-event §14). An error says
|
||||
// the half has no first view yet, never that no module exists.
|
||||
func (v *Views) Halves() (map[string]meshcatalogclient.Half, error) {
|
||||
if v.branches == nil {
|
||||
return nil, fmt.Errorf("the tool runner does not follow the catalog half")
|
||||
}
|
||||
if err := v.branches.Ready(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]meshcatalogclient.Half{}
|
||||
for _, branch := range v.branches.Followed() {
|
||||
if c, ok := v.branches.Catalog(branch); ok {
|
||||
out[branch], _ = c.View() // a branch at none-yet is ready and empty (§614)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Modules is every module the catalog half holds, by name, with its entry: the entries of the watched branches' newest trunk
|
||||
// revisions, the branch followed first naming a module more than one hold (design 47 G §196, §594). An error says the half has
|
||||
// no first view yet, never that no module exists.
|
||||
|
||||
@@ -158,6 +158,10 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
|
||||
t.Fatal(err)
|
||||
}
|
||||
eventually(t, "the catalog-revision was not taken", func() bool { m, err := v.Modules(); return err == nil && len(m) == 2 })
|
||||
// And the halves by branch, with their revision, which mesh_wait names checking an emitter (design 34 every-event §14).
|
||||
if h, err := v.Halves(); err != nil || h["main"].Revision.Revision != 4 || len(h["main"].Entries) != 2 {
|
||||
t.Errorf("the halves: %+v, %v", h, err)
|
||||
}
|
||||
|
||||
// Events: the next revision is applied; an old one passed over; a deletion removes; none of it asks again.
|
||||
asked := func(tool string) int { o.mu.Lock(); defer o.mu.Unlock(); return o.asked[tool] }
|
||||
|
||||
Reference in New Issue
Block a user