Files
mesh-delivery/contracts/go/clients/meshwantsclient/assignment.go
T
jschoubben 18c954ddcd CT10: meshwantsclient holds a part's view of the assignment half, a first view then assignment-revision
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
2026-11-11 11:45:50 +01:00

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)
}