Files
mesh-delivery/modules/mesh-delivery-builder/buildrun/standing.go
T
jschoubben 71a35a6376 MV1: move contracts and sdk under modules/, importers follow
The delivery repository's contracts and SDK live under modules/ as
mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths
renamed to match the folders (the TypeScript names already did), and
every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools
and mesh-sdk follows in the same group: a design as it should be, one
path per module, checked by the group's check building every importer.

Builds: design 47 G §427, §88

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:09:56 +01:00

191 lines
6.5 KiB
Go

package buildrun
import (
"context"
"errors"
"sort"
"sync"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/process"
)
// PlacedNodes holds which modules the catalogue places on which node: the seat mesh-module-assigner's state `assignments`,
// held by the assigner (the holder's `<holder>_assignments`, read by meshmoduleassignerclient.Placed; ADR 0567), or the assignment half
// (meshwantsclient.Placed, fed by Revision). The builder counts the nodes it names (Nodes; ADR 0624 §1). It holds no
// toolchain: the builder keeps the toolchains it heard in its state toolchains-heard and forgets one when the node-engine
// seat says the build module stopped on the node, never on a placement (design 47 G §546).
type PlacedNodes struct {
mu sync.Mutex
logf func(format string, a ...any)
placed placements
known bool // the assignments is settled
// OnPlaced is told, outside the lock, once the assignments's history is read and after each revision of it
// FollowAssignmentsIn hears from then on (the counted nodes, Nodes, may have changed). Nil tells nobody.
OnPlaced func()
}
// placements is where the part reads which modules stand on which node.
type placements interface {
Seeded() bool
ByNode() map[string][]string
}
// NewPlacedNodes is the placements of the builder, saying with logf what it cannot read.
func NewPlacedNodes(logf func(string, ...any)) *PlacedNodes {
return &PlacedNodes{logf: logf, placed: meshmoduleassignerclient.NewPlaced()}
}
// Assignments takes one revision of the seat mesh-module-assigner's assignments in (at is when the bus wrote it) and
// answers whether the nodes the modules stand on changed. A value that does not read is said, and places nothing.
func (g *PlacedNodes) Assignments(key string, value []byte, deleted bool, at time.Time) bool {
g.mu.Lock()
defer g.mu.Unlock()
placed, ok := g.placed.(*meshmoduleassignerclient.Placed)
if !ok {
g.logf("the assignments: %s is not read, the placements take the assignment half (design 47)", key)
return false
}
was := placed.Seeded()
_, changed, err := placed.Put(key, value, deleted, at)
if err != nil {
g.logf("the assignments: %v (issue 1940)", err)
}
if !g.known {
return false
}
return was != g.placed.Seeded() || len(changed) > 0
}
// Revision takes one assignment graph revision the part's view holds in (design 47 *Each part acts on its own entries*:
// the placements come from the seat mesh-wants' assignment half, never the assigner's state) and answers whether the
// nodes the modules stand on changed. The first revision taken settles it. Placements fed by Revision read no Assignments.
func (g *PlacedNodes) Revision(r meshwants.AssignmentRevision) bool {
g.mu.Lock()
defer g.mu.Unlock()
placed, ok := g.placed.(*meshwantsclient.Placed)
if !ok {
placed = meshwantsclient.NewPlaced()
g.placed = placed
}
was := placed.Seeded()
changed := placed.Take(r)
if changed == nil {
return false
}
first := !g.known
g.known = true
return first || was != placed.Seeded() || len(changed) > 0
}
// Nodes are the nodes the assignments places any module on, sorted: the nodes the mesh counts (a node leaves the count
// when the assignments no longer names it; ADR 0624 §1). None until the assignments is read.
func (g *PlacedNodes) Nodes() []string {
var out []string
for node, modules := range g.placed.ByNode() {
if len(modules) > 0 {
out = append(out, node)
}
}
sort.Strings(out)
return out
}
// Settled says the assignments's history is read.
func (g *PlacedNodes) Settled() {
g.mu.Lock()
defer g.mu.Unlock()
g.known = true
}
// assignmentsFollowed is what FollowAssignmentsIn feeds: the builder's PlacedNodes, or the ToolchainGate of the checker and the
// indexer. settle says the history is read and answers whether what the part passes on changed; placedTold tells its OnPlaced.
type assignmentsFollowed interface {
Assignments(key string, value []byte, deleted bool, at time.Time) bool
settle() bool
placedTold()
}
func (g *PlacedNodes) settle() bool { g.Settled(); return true }
func (g *PlacedNodes) placedTold() {
if g.OnPlaced != nil {
g.OnPlaced()
}
}
func (g *ToolchainGate) settle() bool { return g.Settled() }
func (g *ToolchainGate) placedTold() {
if g.OnPlaced != nil {
g.OnPlaced()
}
}
// FollowAssignmentsIn reads the seat mesh-module-assigner's assignments, the state `bucket` of its holder (found at run time, design 47
// G A-119, A-126: AssignmentsOf), with its history into gate, says it settled, and follows it; changed is told whenever what the
// gate passes on changed. Where the state cannot be read or watched (not there, or this part may not read it) it says so and the gate
// passes every report on: not fatal, since an indexer, builder or checker that stops for it stops every delivery (issue 1945). An
// error of the watch after that ends on the channel.
func FollowAssignmentsIn(ctx context.Context, js process.BucketOpener, bucket string, gate assignmentsFollowed, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) {
errs := make(chan error, 1)
kv, err := js.KeyValue(ctx, bucket)
if err != nil {
logf("the state %s cannot be read, so no node is counted by the assignments: %v", bucket, err)
return errs, nil
}
w, err := kv.WatchAll(ctx, jetstream.IncludeHistory())
if err != nil {
logf("the state %s cannot be watched, so no node is counted by the assignments: %v", bucket, err)
return errs, nil
}
put := func(e jetstream.KeyValueEntry) bool {
return gate.Assignments(e.Key(), e.Value(), e.Operation() != jetstream.KeyValuePut, e.Created())
}
for settled := false; !settled; {
select {
case <-ctx.Done():
_ = w.Stop()
return errs, ctx.Err()
case e, ok := <-w.Updates():
if !ok {
return errs, errors.New("the watch of " + bucket + " ended before it settled")
}
if e == nil {
settled = true
continue
}
put(e)
}
}
if gate.settle() {
changed(ctx)
}
gate.placedTold()
go func() {
defer func() { _ = w.Stop() }()
for {
select {
case <-ctx.Done():
errs <- ctx.Err()
return
case e, ok := <-w.Updates():
if !ok {
errs <- errors.New("the watch of " + bucket + " ended")
return
}
if e != nil && put(e) {
changed(ctx)
}
if e != nil {
gate.placedTold()
}
}
}
}()
return errs, nil
}