provisioner_compose took 2.6-13.6 s per node with nothing changed, CPU bound. The node is now composed at its wanted and its newest builds over one read (frombus.PlansHeldTwice, whose composer resolves each machine once), its own plans, provisions and refusals and the providers' entries are read from the watched copy, its own writes taken in at once, and a provider's manifest once by digest: no read by key per composition is left. The result says where the time went, phase by phase, and a slow composition that changed nothing says it in the log. BenchmarkComposeNode times it over a mesh captured with the read tools (MESH_COMPOSE_CAPTURE): novox 11.7 s to 0.49 s, ace 0.49 s, g14 and shanks 0.39 s; plans and provisions identical to before over nine compositions of the four nodes. Needs mesh-sdk feat/compose-once (5d26c65); go.mod still pins the old mesh-sdk until the group pins its head. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
233 lines
8.1 KiB
Go
233 lines
8.1 KiB
Go
// mesh-provisioner: the provisioner's server, holder of the mesh-provisioner seat once for the mesh (novox/hq ADR
|
|
// 0349 clause 3, ADR 0570). A Go bundle served by a runtime of its own, on its own bus account (`runs-as`, ADR 0259
|
|
// §8), keeping mesh-provisioner_provisions and _plans (every provision and every plan, composed from the bus on change
|
|
// events, its alone), _registry (which domain answers which kind of placeholder, its alone), and _applied and
|
|
// _collect, which each machine's provisioner client writes for its own machine; _needs is retired and emptied at its
|
|
// start. stdout is the MCP channel; what this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Seat is the role this module holds, once for the mesh, and Verb its one verb.
|
|
const (
|
|
Seat = "mesh-provisioner"
|
|
Module = "mesh-provisioner"
|
|
Verb = "provisioning"
|
|
)
|
|
|
|
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
|
|
|
|
// runtimeStore is the bus, through the runtime that launched this bundle.
|
|
type runtimeStore struct{}
|
|
|
|
func (runtimeStore) Get(state, key string) (*Entry, error) {
|
|
e, err := stdio.State(state).Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
return &Entry{Key: e.Key, Value: e.Value, Revision: e.Revision}, nil
|
|
}
|
|
|
|
func (runtimeStore) Put(state, key string, value any) error {
|
|
_, err := stdio.State(state).Put(key, value)
|
|
return err
|
|
}
|
|
|
|
// PutRevision is Put, saying the revision it landed at.
|
|
func (runtimeStore) PutRevision(state, key string, value any) (uint64, error) {
|
|
return stdio.State(state).Put(key, value)
|
|
}
|
|
|
|
func (runtimeStore) Delete(state, key string) error { return stdio.State(state).Delete(key) }
|
|
|
|
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
|
|
|
// runtimeAsks is the operator's channel through the runtime: asks published under this module's own name, and the
|
|
// router's record of one read under that name.
|
|
type runtimeAsks struct{}
|
|
|
|
func (runtimeAsks) Publish(subject string, body any, id string) error {
|
|
return stdio.SeatPublish(subject, body, id)
|
|
}
|
|
|
|
func (runtimeAsks) Record(id string) ([]byte, bool, error) {
|
|
e, err := stdio.SeatRecord("", id)
|
|
if err != nil || e == nil {
|
|
return nil, false, err
|
|
}
|
|
return e.Value, true, nil
|
|
}
|
|
|
|
// work is the nodes a change touched, composed one at a time by one worker: no timer, a change's event is the wake.
|
|
type work struct {
|
|
mu sync.Mutex
|
|
pending map[string]bool
|
|
wake chan struct{}
|
|
}
|
|
|
|
func (w *work) add(nodes ...string) {
|
|
if len(nodes) == 0 {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
for _, n := range nodes {
|
|
w.pending[n] = true
|
|
}
|
|
w.mu.Unlock()
|
|
select {
|
|
case w.wake <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (w *work) run(h *Holder) {
|
|
for range w.wake {
|
|
w.mu.Lock()
|
|
nodes := sortedKeys(w.pending)
|
|
w.pending = map[string]bool{}
|
|
w.mu.Unlock()
|
|
for _, n := range nodes {
|
|
c, err := h.ComposeNode(context.Background(), n)
|
|
switch {
|
|
case err != nil:
|
|
logf("[%s] %s is not composed (%s): %v", Module, n, TookSaid(c.Took), err)
|
|
case len(c.Plans)+len(c.Gone)+len(c.Changed)+len(c.Asked)+len(c.Problems) > 0:
|
|
logf("[%s] composed %s: %+v", Module, n, c)
|
|
case slowest(c.Took) >= slowAt:
|
|
// Nothing changed and it took long: where the time went is said, so a slow composition is seen (issue 1954).
|
|
logf("[%s] composed %s, nothing changed: %s", Module, n, TookSaid(c.Took))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// slowAt is how long a composition that changed nothing may take before its phases are said.
|
|
const slowAt = time.Second
|
|
|
|
// slowest is a composition's whole time: its last phase, "whole".
|
|
func slowest(took []Took) time.Duration {
|
|
if len(took) == 0 {
|
|
return 0
|
|
}
|
|
return time.Duration(took[len(took)-1].MS) * time.Millisecond
|
|
}
|
|
|
|
func main() {
|
|
s := runtimeStore{}
|
|
ws := stdio.NewWatchedStates() // one copy of what it reads, each state by its watch (ADR 0580 clause 2)
|
|
h := &Holder{Store: Watching(s, ws), Compose: FromWatched(ws), Asks: runtimeAsks{}, Now: time.Now, Logf: logf}
|
|
w := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)}
|
|
go w.run(h)
|
|
go starting(h)
|
|
// Each watch's handler runs after the copy took its change, so a composition it wakes reads the copy that change made.
|
|
go ws.Follow(PlacementRead, func(c stdio.StateChange) error { w.add(NodesOfPlacement(c.Key)...); return nil })
|
|
go ws.Follow(WantsRead, func(c stdio.StateChange) error { w.add(NodesOfWant(c.Key)...); return nil })
|
|
go ws.Follow(ResolvedRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
|
|
go ws.Follow(HaveRead, func(c stdio.StateChange) error { w.add(NodesOfNodeKey(c.Key)...); return nil })
|
|
go ws.Follow(ModulesRead, func(c stdio.StateChange) error {
|
|
if module, err := delivery.ParseModulesKey(c.Key); err == nil {
|
|
w.add(h.NodesOfModule(module)...)
|
|
}
|
|
return nil
|
|
})
|
|
for _, provider := range BoundReads {
|
|
// Into the copy the holder reads them from, its handler run after the copy took the change.
|
|
go ws.Follow(boundRead(provider), func(c stdio.StateChange) error {
|
|
if said, err := h.Rejudge(c.Key); err != nil {
|
|
logf("[%s] %s is not judged again: %v", Module, c.Key, err)
|
|
} else if said != "" {
|
|
logf("[%s] %s", Module, said)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
go stdio.State(Registry).Follow("", func(c stdio.StateChange) error {
|
|
if c.Op != "put" {
|
|
if _, _, err := Publish(s); err != nil {
|
|
logf("[%s] the registry is not written back: %v", Module, err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
go func() {
|
|
<-stdio.Served() // subscribed as soon as the runtime serves, never after a guessed wait: an event sent meanwhile is heard
|
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
|
err := stdio.Subscribe(asks.Seat+".decided."+Module, func(stdio.Envelope) error {
|
|
for _, said := range h.Decide() {
|
|
logf("[%s] %s", Module, said)
|
|
}
|
|
return nil
|
|
})
|
|
if err == nil {
|
|
return
|
|
}
|
|
logf("[%s] does not hear the operator's answers yet (%v); asking again in %s", Module, err, wait)
|
|
time.Sleep(wait)
|
|
}
|
|
}()
|
|
if err := stdio.Serve("", append(Tools(s), holderTools(h)...)); err != nil {
|
|
logf("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// starting writes the registry, deletes the retired needs and reads the operator's answers once the runtime answers,
|
|
// each tried again until it lands.
|
|
func starting(h *Holder) {
|
|
<-stdio.Served()
|
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
|
written, deleted, err := Publish(h.Store)
|
|
if err == nil {
|
|
if len(written)+len(deleted) > 0 {
|
|
logf("[%s] the registry: wrote %v, deleted %v", Module, written, deleted)
|
|
}
|
|
break
|
|
}
|
|
logf("[%s] the registry is not written yet (%v); trying again in %s", Module, err, wait)
|
|
time.Sleep(wait)
|
|
}
|
|
if n, err := h.RetireNeeds(); err != nil {
|
|
logf("[%s] the retired needs are not all deleted: %v", Module, err)
|
|
} else if n > 0 {
|
|
logf("[%s] deleted %d key(s) of the retired needs", Module, n)
|
|
}
|
|
for _, said := range h.Decide() {
|
|
logf("[%s] %s", Module, said)
|
|
}
|
|
}
|
|
|
|
// Tools is the seat's verb as the runtime serves it.
|
|
func Tools(s Store) []stdio.Tool {
|
|
return []stdio.Tool{{
|
|
Name: Seat + "." + Verb,
|
|
Description: "Read what the provisioner keeps (novox/hq ADR 0349 clause 3): the registry (which domain " +
|
|
"answers which kind of placeholder, the default), the needs each module on each node recorded, what is " +
|
|
"applied where, or the items of every collection, narrowed by node and module, and collect by " +
|
|
"collection. Reads only: each node's provisioner client writes its own node's needs, applied and items.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"what": map[string]any{"type": "string", "enum": []string{Registry, Needs, Applied, Collect},
|
|
"description": "registry when not given"},
|
|
"node": map[string]any{"type": "string", "description": "a node"},
|
|
"module": map[string]any{"type": "string", "description": "a module: for collect, a contributor"},
|
|
"collection": map[string]any{"type": "string", "description": "for collect: a collection"},
|
|
}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
q, err := queryOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Do(s, q)
|
|
},
|
|
}}
|
|
}
|