The typed first view and the following of each half sit in the seats' client packages, built on go/views (design 47 G §245), so no part follows the assignment half by a follower of its own: a newer trunk revision heard is waited for, said in the status, asked and held; a candidate's, or one held, asks nothing. Builds: design 47 G §245, §365 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
177 lines
7.0 KiB
Go
177 lines
7.0 KiB
Go
package meshwantsclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
)
|
|
|
|
// RevisionVerb is the seat verb a part takes its first view of the assignment half by, as its manifest's invokes names
|
|
// it.
|
|
const RevisionVerb = "seat:" + seat.Seat + "." + seat.VerbRevision
|
|
|
|
// kindTrunk is the kind of an assignment graph revision a running part acts on; a candidate's moves nothing.
|
|
const kindTrunk = "trunk"
|
|
|
|
// Call asks one seat verb over the bus and answers what the holder said, whatever wraps it (process.Conn.Call, or
|
|
// stdio.Ask through the tool runner).
|
|
type Call func(key string, body any) (json.RawMessage, error)
|
|
|
|
// assignmentCreated stands for the view's Created in its follower: a revision view is never made again, so one fixed
|
|
// time, never zero, lets the follower tell a revision heard from one it holds.
|
|
var assignmentCreated = time.Unix(0, 0).UTC()
|
|
|
|
// assignmentViewName is how an assignment half's revision is named in a part's first-view status (views.Status.View,
|
|
// WaitsFor).
|
|
func assignmentViewName(revision int64) string { return strconv.FormatInt(revision, 10) }
|
|
|
|
// Assignment is a part's view of the assignment half (design 47 G §245): a first view of the newest trunk revision asked
|
|
// of the seat mesh-wants' verb `revision`, then kept from its event `assignment-revision`, built on go/views' Follower,
|
|
// so the runtime's tools `<module>_first_views` and `<module>_first_view_again` read and steer it as every first view.
|
|
// Its picture is keyed `<node>.<module>`, each value that entry's reference. With no view the part acts on nothing:
|
|
// Revision says false, and a revision heard and not held yet is waited for, said in the status, and past
|
|
// views.WaitsBound raised as view-waits.
|
|
type Assignment struct {
|
|
Follower *views.Follower
|
|
// Held, when set, is told of each revision once the view holds it, on the ask's goroutine and outside any lock.
|
|
Held func(seat.AssignmentRevision)
|
|
|
|
call Call
|
|
mu sync.Mutex
|
|
held *seat.AssignmentRevision
|
|
waits int64
|
|
}
|
|
|
|
// NewAssignment is an Assignment asking the seat through call. Nothing is asked before Start.
|
|
func NewAssignment(call Call) *Assignment {
|
|
a := &Assignment{call: call}
|
|
a.Follower = &views.Follower{Owner: seat.Seat, Tool: RevisionVerb, State: seat.StateAssignmentRevisions, Ask: a.ask}
|
|
return a
|
|
}
|
|
|
|
// WireAssignment is an Assignment adopted by a stdio part's views.Reader: the reader writes its conditions and its
|
|
// view-waits finding, reads it as `mesh-wants.assignment-revisions`, lists and asks it again in its first-view tools, and
|
|
// at its Start subscribes the seat's event assignment-revision (the part's manifest consumes it) into Hear and asks the
|
|
// first view. held, when not nil, is the Assignment's Held. The part declares the seat's verb revision in invokes.
|
|
func WireAssignment(r *views.Reader, call Call, held func(seat.AssignmentRevision)) (*Assignment, error) {
|
|
a := NewAssignment(call)
|
|
a.Held = held
|
|
if err := r.Adopt(a.Follower, seat.Seat, seat.EventAssignmentRevision, a.Hear); err != nil {
|
|
return nil, err
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// WireAssignmentProcess is WireAssignment for a process's views.ProcessReader: its picture
|
|
// (`mesh-wants_assignment-revisions`) reads the half as a read-only bucket, and the seat's event assignment-revision on
|
|
// the module's durable consumer goes to Hear.
|
|
func WireAssignmentProcess(r *views.ProcessReader, call Call, held func(seat.AssignmentRevision)) (*Assignment, *views.Picture, error) {
|
|
a := NewAssignment(call)
|
|
a.Held = held
|
|
p, err := r.Adopt(a.Follower, seat.Seat, seat.EventAssignmentRevision, a.Hear)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return a, p, nil
|
|
}
|
|
|
|
// Start asks the first view, the newest trunk revision, until it is answered or ctx ends.
|
|
func (a *Assignment) Start(ctx context.Context) error { return a.Follower.Start(ctx) }
|
|
|
|
// Revision is the assignment graph revision the view holds; false while it has none, when the part acts on nothing new.
|
|
func (a *Assignment) Revision() (seat.AssignmentRevision, bool) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
if a.held == nil {
|
|
return seat.AssignmentRevision{}, false
|
|
}
|
|
return *a.held, true
|
|
}
|
|
|
|
// Hear takes one `assignment-revision` event, as the part's durable consumer of the seat's events hands it. A
|
|
// candidate's revision moves nothing; a trunk revision the view holds, or one older, is passed over; a newer one is
|
|
// waited for and asked on its own goroutine, so the consumer is never held while the holder is away. Data that is no
|
|
// such event is an error.
|
|
func (a *Assignment) Hear(ctx context.Context, data []byte) error {
|
|
var ev seat.AssignmentRevisionEvent
|
|
if err := json.Unmarshal(data, &ev); err != nil || ev.Revision <= 0 {
|
|
return fmt.Errorf("meshwantsclient: not a %s event: %s", seat.EventAssignmentRevision, clipBytes(data))
|
|
}
|
|
if ev.Kind != kindTrunk {
|
|
return nil
|
|
}
|
|
a.mu.Lock()
|
|
held := int64(0)
|
|
if a.held != nil {
|
|
held = a.held.Revision
|
|
}
|
|
if ev.Revision > a.waits {
|
|
a.waits = ev.Revision
|
|
}
|
|
a.mu.Unlock()
|
|
if ev.Revision <= held {
|
|
return nil
|
|
}
|
|
// The follower keeps the newest revision heard, so an ask that was running when it was heard is asked once more.
|
|
a.Follower.Apply(views.Event{Owner: seat.Seat, State: seat.StateAssignmentRevisions, Revision: uint64(ev.Revision), Created: assignmentCreated, Gap: true})
|
|
a.Follower.WaitFor(ctx, assignmentViewName(ev.Revision))
|
|
go func() {
|
|
_ = a.Follower.Gap(ctx, fmt.Sprintf("assignment revision %d said, the view holds %d", ev.Revision, held))
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// ask is the Follower's Ask: the newest trunk revision, whole in one answer (its entries are named by digest).
|
|
func (a *Assignment) ask(_ context.Context, _ string) (views.Answer, error) {
|
|
raw, err := a.call(RevisionVerb, seat.RevisionInput{})
|
|
if err != nil {
|
|
return views.Answer{}, err
|
|
}
|
|
answer, said, ok := buscalls.Unwrap(raw)
|
|
if !ok {
|
|
return views.Answer{}, fmt.Errorf("%s answered: %s", RevisionVerb, said)
|
|
}
|
|
var ans seat.RevisionAnswer
|
|
if err := json.Unmarshal(answer, &ans); err != nil {
|
|
return views.Answer{}, fmt.Errorf("%s answered what is not its answer: %s", RevisionVerb, clipBytes(answer))
|
|
}
|
|
r := ans.Revision
|
|
if r.Revision <= 0 || r.Kind != kindTrunk {
|
|
return views.Answer{}, fmt.Errorf("%s answered a %q revision %d for the trunk", RevisionVerb, r.Kind, r.Revision)
|
|
}
|
|
values := make(map[string]json.RawMessage, len(r.Entries))
|
|
for _, e := range r.Entries {
|
|
v, _ := json.Marshal(e)
|
|
values[e.Node+"."+e.Module] = v
|
|
}
|
|
a.mu.Lock()
|
|
moved := a.held == nil || a.held.Revision != r.Revision
|
|
a.held = &r
|
|
holds := a.waits <= r.Revision
|
|
if holds {
|
|
a.waits = 0
|
|
}
|
|
a.mu.Unlock()
|
|
if holds {
|
|
a.Follower.Holds(assignmentViewName(r.Revision))
|
|
}
|
|
if moved && a.Held != nil {
|
|
a.Held(r)
|
|
}
|
|
return views.Answer{V: 1, Owner: seat.Seat, State: seat.StateAssignmentRevisions, Revision: uint64(r.Revision), Created: assignmentCreated, Values: values, At: time.Now()}, nil
|
|
}
|
|
|
|
func clipBytes(b []byte) string {
|
|
if len(b) > 200 {
|
|
return string(b[:200]) + "…"
|
|
}
|
|
return string(b)
|
|
}
|