Merge branch 'feat/47-G-fix11-a5' into feat/47-G-join11
# Conflicts: # node-tools/internal/firstview/firstview_test.go # node-tools/internal/runtime/announce_test.go # node-tools/internal/runtime/busstate.go # node-tools/internal/runtime/ledgers_test.go
This commit is contained in:
@@ -20,39 +20,41 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
||||
catalogseat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"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"
|
||||
)
|
||||
|
||||
// The owner and its first-view tools (the tools' addresses are `<owner>.<tool>`).
|
||||
const Assigner = "mesh-delivery-assigner"
|
||||
// AssignerSeat is the seat whose holder's views the runner follows: the holder is read from the catalog half's claims of the
|
||||
// seat, never named in code (design 47 G §616, §428), and the names the console reads the two states by are the seat's
|
||||
// (`<seat>.<state>`; the manifest's `consumes` names their views families by the seat, as `seat:<seat>.views.<state>`).
|
||||
const AssignerSeat = meshmoduleassigner.Seat
|
||||
|
||||
// The names the console reads the assigner's two states by (`<owner>.<state>`), as the manifest's `consumes` names their views
|
||||
// families (no seat's client names a holder, novox/hq ADR 0625 §4; TestTheFirstViewsAreTheManifestsViews).
|
||||
const (
|
||||
AssignmentsRead = Assigner + ".assignments"
|
||||
SeatHoldersRead = Assigner + ".seat-holders"
|
||||
AssignmentsRead = AssignerSeat + ".assignments"
|
||||
SeatHoldersRead = AssignerSeat + ".seat-holders"
|
||||
)
|
||||
|
||||
// Spec is one first view the runner holds.
|
||||
type Spec struct{ Owner, Tool, State string }
|
||||
// Spec is one first view the runner holds of the seat's holder: its first-view tool and the state.
|
||||
type Spec struct{ Tool, State string }
|
||||
|
||||
// Specs are the two owner states asked of an owner's tool; the catalog half's views are adopted by the branch follower.
|
||||
// Specs are the two states asked of the holder's tool; the catalog half's views are adopted by the branch follower.
|
||||
var Specs = []Spec{
|
||||
{Assigner, "assignments", "assignments"},
|
||||
{Assigner, "seat-holders", "seat-holders"},
|
||||
{"assignments", "assignments"},
|
||||
{"seat-holders", "seat-holders"},
|
||||
}
|
||||
|
||||
// Name is the name a state is read by, `<owner>.<state>`: AssignmentsRead and SeatHoldersRead.
|
||||
func (s Spec) Name() string { return s.Owner + "." + s.State }
|
||||
// Name is the name a state is read by, `<seat>.<state>`: AssignmentsRead and SeatHoldersRead.
|
||||
func (s Spec) Name() string { return AssignerSeat + "." + s.State }
|
||||
|
||||
// Views is the first views: the assigner's two, and the catalog half's per watched branch.
|
||||
type Views struct {
|
||||
@@ -63,6 +65,7 @@ type Views struct {
|
||||
adopted map[string]*adoption // by `<owner>.<state>`, the views the branch follower adopted
|
||||
hearers map[string][]*adoption // event key `<seat>.<event>` -> the adopted views that hear it
|
||||
branches *meshcatalogclient.Branches
|
||||
holder *holding // the module that claims the assigner seat, once the catalog half names it
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
@@ -89,6 +92,9 @@ type Deps struct {
|
||||
Findings FindingWriter
|
||||
// StartFindings starts that writer, and its stop ends it; nil starts none.
|
||||
StartFindings func(ctx context.Context) (stop func(), err error)
|
||||
// Build is the build this process was started with (process.OwnBuild, read once at the start): its fingerprint, or
|
||||
// why it is unknown (design 47 G §205, §424).
|
||||
Build process.Build
|
||||
// Node is the machine the runner is on, named in its findings' subject.
|
||||
Node string
|
||||
// Bound and Wait are the follower's (zero: two minutes, five seconds).
|
||||
@@ -111,7 +117,7 @@ func Start(parent context.Context, conn *bus.Conn, logf func(string, ...any)) (*
|
||||
// state adapter, JetStream and node are this connection's, in this one place (design 47 G A-16, A-27).
|
||||
func DepsOf(conn *bus.Conn, logf func(string, ...any)) Deps {
|
||||
me := conn.Module()
|
||||
d := Deps{Module: me, Node: conn.Node(),
|
||||
d := Deps{Module: me, Node: conn.Node(), Build: process.OwnBuild(),
|
||||
Ask: func(address string, body any) (json.RawMessage, error) {
|
||||
got, err := conn.Ask(address, body, "")
|
||||
return got.Result, err
|
||||
@@ -168,22 +174,6 @@ func StartWith(parent context.Context, d Deps, logf func(string, ...any)) (*View
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
v := &Views{logf: logf, ctx: ctx, cancel: cancel, followers: map[string]*views.Follower{}, byState: map[string]*views.Follower{},
|
||||
adopted: map[string]*adoption{}, hearers: map[string][]*adoption{}, deps: d}
|
||||
for _, s := range Specs {
|
||||
s := s
|
||||
call := func(tool string, body any) (json.RawMessage, error) { return d.Ask(s.Owner+"."+tool, body) }
|
||||
f := &views.Follower{Owner: s.Owner, Tool: s.Tool, State: s.State, Ask: views.AskVia(call, s.Tool), Logf: logf,
|
||||
Bound: d.Bound, Wait: d.Wait,
|
||||
Raise: func(st views.Status) {
|
||||
v.finding(unansweredFinding(v.deps.Module, v.deps.Node, st, time.Now()), st.Tool, true)
|
||||
},
|
||||
Clear: func(st views.Status) {
|
||||
v.finding(unansweredFinding(v.deps.Module, v.deps.Node, st, time.Now()), st.Tool, false)
|
||||
}}
|
||||
v.followers[s.Name()] = f
|
||||
v.byState[s.State] = f
|
||||
v.wg.Add(1)
|
||||
go func() { defer v.wg.Done(); _ = f.Start(ctx) }()
|
||||
}
|
||||
stopFindings := func() {}
|
||||
if d.StartFindings != nil {
|
||||
if stop, err := d.StartFindings(ctx); err != nil {
|
||||
@@ -199,6 +189,9 @@ func StartWith(parent context.Context, d Deps, logf func(string, ...any)) (*View
|
||||
logf("[mesh-tools] the catalog half is not followed (%v): the console cannot say which modules exist", err)
|
||||
} else {
|
||||
branches.Logf = func(format string, args ...any) { logf("[mesh-tools] "+format, args...) }
|
||||
branches.Changed = func(_, module string, entry json.RawMessage, deleted bool, _ time.Time) {
|
||||
v.claimed(module, entry, deleted)
|
||||
}
|
||||
v.branches = branches
|
||||
}
|
||||
stop, err := d.Consume(v.hear)
|
||||
@@ -228,7 +221,9 @@ func (v *Views) hear(env bus.Envelope) error {
|
||||
if err != nil {
|
||||
return nil // not a views event: nothing to hold, nothing to retry
|
||||
}
|
||||
v.mu.Lock()
|
||||
f := v.byState[ev.State]
|
||||
v.mu.Unlock()
|
||||
if f == nil || f.Owner != ev.Owner {
|
||||
return nil
|
||||
}
|
||||
@@ -261,6 +256,13 @@ func (v *Views) finding(f condseat.Finding, tool string, raised bool) {
|
||||
// not answered yet, so nothing is claimed of it.
|
||||
func (v *Views) State(name string) (map[string][]byte, error) {
|
||||
v.mu.Lock()
|
||||
if state, ok := strings.CutPrefix(name, AssignerSeat+"."); ok { // the seat's name stands for its holder's
|
||||
if v.holder == nil {
|
||||
v.mu.Unlock()
|
||||
return nil, fmt.Errorf("the catalog half names no holder of the seat %s yet, so %s has no first view", AssignerSeat, name)
|
||||
}
|
||||
name = v.holder.module + "." + state
|
||||
}
|
||||
f := v.followers[name]
|
||||
v.mu.Unlock()
|
||||
if f == nil {
|
||||
@@ -297,6 +299,10 @@ func (v *Views) Status() []views.Status {
|
||||
return out
|
||||
}
|
||||
|
||||
// OwnBuild is the build the tool runner was started with: the fingerprint of the want its node-engine applied, or why it is
|
||||
// unknown (`own build unknown: started without MESH_FINGERPRINT`).
|
||||
func (v *Views) OwnBuild() process.Build { return v.deps.Build }
|
||||
|
||||
// Entries are the entries by fingerprint the catalog half's follower keeps, with their state and since when (design 47 G A-103).
|
||||
func (v *Views) Entries() any {
|
||||
if v.branches == nil {
|
||||
@@ -489,3 +495,78 @@ func (v *Views) Modules() (map[string]catalogseat.CatalogEntry, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// holding is the views of the module that claims the assigner seat, and what stops them.
|
||||
type holding struct {
|
||||
module string
|
||||
cancel context.CancelFunc
|
||||
views []*views.Follower
|
||||
}
|
||||
|
||||
// claimed is the branch follower's word of one entry changed: an entry that serves a verb of the assigner seat makes its module
|
||||
// the holder whose views the runner follows (a holder before it is dropped), the deletion of the holder's entry drops them
|
||||
// (design 47 G §616, §428). It acts on the event alone; nothing waits on a timer for the claim.
|
||||
func (v *Views) claimed(module string, entry json.RawMessage, deleted bool) {
|
||||
claims := false
|
||||
if !deleted {
|
||||
var e catalogseat.CatalogEntry
|
||||
if json.Unmarshal(entry, &e) == nil {
|
||||
claims = slices.ContainsFunc(e.Serves, func(s string) bool { return strings.HasPrefix(s, AssignerSeat+".") })
|
||||
}
|
||||
}
|
||||
v.mu.Lock()
|
||||
cur := v.holder
|
||||
switch {
|
||||
case claims && cur != nil && cur.module == module, !claims && (cur == nil || cur.module != module), v.ctx.Err() != nil:
|
||||
v.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if cur != nil {
|
||||
v.dropHolderLocked()
|
||||
}
|
||||
if claims {
|
||||
v.startHolderLocked(module)
|
||||
}
|
||||
v.mu.Unlock()
|
||||
}
|
||||
|
||||
// startHolderLocked follows the first views of module, the holder of the seat, on the runner's own consumer.
|
||||
func (v *Views) startHolderLocked(module string) {
|
||||
ctx, cancel := context.WithCancel(v.ctx)
|
||||
h := &holding{module: module, cancel: cancel}
|
||||
d := v.deps
|
||||
for _, s := range Specs {
|
||||
s := s
|
||||
call := func(tool string, body any) (json.RawMessage, error) { return d.Ask(module+"."+tool, body) }
|
||||
f := &views.Follower{Owner: module, Tool: s.Tool, State: s.State, Ask: views.AskVia(call, s.Tool), Logf: v.logf,
|
||||
Bound: d.Bound, Wait: d.Wait,
|
||||
Raise: func(st views.Status) {
|
||||
v.finding(unansweredFinding(d.Module, d.Node, st, time.Now()), st.Tool, true)
|
||||
},
|
||||
Clear: func(st views.Status) {
|
||||
v.finding(unansweredFinding(d.Module, d.Node, st, time.Now()), st.Tool, false)
|
||||
}}
|
||||
v.followers[module+"."+s.State] = f
|
||||
v.byState[s.State] = f
|
||||
h.views = append(h.views, f)
|
||||
v.wg.Add(1)
|
||||
go func() { defer v.wg.Done(); _ = f.Start(ctx) }()
|
||||
}
|
||||
v.holder = h
|
||||
v.logf("[mesh-tools] the seat %s is held by %s: its views are followed", AssignerSeat, module)
|
||||
}
|
||||
|
||||
// dropHolderLocked stops the views of the holder and clears the findings they raised.
|
||||
func (v *Views) dropHolderLocked() {
|
||||
h := v.holder
|
||||
v.holder = nil
|
||||
h.cancel()
|
||||
for _, f := range h.views {
|
||||
delete(v.followers, f.Owner+"."+f.State)
|
||||
delete(v.byState, f.State)
|
||||
if st := f.Status(); st.Raised {
|
||||
go v.finding(unansweredFinding(v.deps.Module, v.deps.Node, st, time.Now()), st.Tool, false)
|
||||
}
|
||||
}
|
||||
v.logf("[mesh-tools] %s no longer holds the seat %s: its views are no longer followed", h.module, AssignerSeat)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user