Files
mesh-host/internal/selfcompose/composedstate.go
T
jschoubben 4f51393b72 Publish every composition the node makes in composed-<node> (H3)
ADR 0593 §6: the provisioner, the assigner and the view read compositions
instead of composing, so the node's engine writes each one it makes to
its composition state: the wanted builds, every placed module at its
newest done build (so the plan exists before the want moves), each with
its provisioning plan, and a module refused alone with its refusal. A
key is written only when it differs, and kept while among the module's
five newest or named by a want. A placed module's trunk build landing
now makes a pass.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:29:20 +01:00

321 lines
11 KiB
Go

package selfcompose
import (
"bytes"
"context"
"fmt"
"maps"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/compose/frombus"
)
// **The compositions this node makes, published in its composition state** (novox/hq ADR 0593 §6). The node's engine
// alone composes, and it writes every composition it makes into `composed-<node>` (delivery.ComposedBucketFor), a
// record for the provisioner, the assigner and the view, never an instruction: the engine applies only what it composes
// itself, never a value read from the state, so a value carries no signature. After a pass that composed, it writes:
//
// - each module at the fingerprint its want names, as the agent applies it (`<node>.<module>.<fp>`), and the node's own
// composition (`<node>.node`) when its want exists;
// - each module placed on the node at its newest done build: the indexer's fingerprint once the builder holds a done
// build of it, so the plan exists before the want moves (build done, the node composes and writes, the provisioner
// reads, the providers provide, the assigner moves the want); a module it runs that no want names, at what runs;
// - each module's plan (catalogue.Plan) exactly when the provisioner's plans name it, from the same pass's builds;
// - a module refused alone at a fingerprint as that key with its refusal (Refused), and no resources.
//
// A key is written only when its value differs from what the state holds (read once per run), and a module's key is
// deleted once its fingerprint is not among the five it wrote last (delivery.ComposedKeep) and no want names it. A pass
// refused whole, or a bus that did not answer, writes nothing: what was written stands, and each module's have says why.
// ComposedStore is this node's composition state, bound and never created (the grants part creates it).
type ComposedStore interface {
// Entries is every key the state holds with its value and revision.
Entries(ctx context.Context) (map[string]ComposedEntry, error)
Put(ctx context.Context, key string, value []byte) (revision uint64, err error)
Delete(ctx context.Context, key string) error
}
// ComposedEntry is one key's value and revision in the state.
type ComposedEntry struct {
Value []byte
Revision uint64
}
// published is what a pass composed at the wanted builds, for publishComposed: under mu.
type published struct {
scope frombus.Scope
fps map[string]string // by module, the wanted fingerprint
comps map[string]delivery.Composed
held map[string]string
// placed is the modules the node's placement puts here, ran what runs of each module (by its composition's FP).
placed []string
ran map[string]string
}
// publishComposed writes the node's composition state from the last pass that composed, when one asked it (pubDue).
func (c *Composer) publishComposed(ctx context.Context) {
c.mu.Lock()
due, at := c.pubDue, c.pubAt
c.pubDue = false
c.mu.Unlock()
if !due || c.env.ComposedState == nil || at == nil {
return
}
wrote, deleted, err := c.writeComposed(ctx, at)
if err != nil {
c.sayComposed("this node's compositions are not kept in its composition state; " + c.oweComposed() + ": " + err.Error())
return
}
c.mu.Lock()
c.pubTries = 0
c.mu.Unlock()
c.sayComposed("")
if wrote+deleted > 0 {
c.env.Say(fmt.Sprintf("this node's composition state: %d key(s) written, %d deleted", wrote, deleted))
}
}
// newestFPs is, of the modules placed here, the fingerprint each is composed at for the state beyond the wanted ones:
// the indexer's, where the builder holds a done build of it; else the want's; else what runs of it. A module with none
// is left out.
func (c *Composer) newestFPs(ctx context.Context, at *published) map[string]string {
out := map[string]string{}
r := reader{c: c}
for _, module := range at.placed {
c.mu.Lock()
fp2 := c.modules[module].FP2
c.mu.Unlock()
if fp2 != "" {
if b, err := r.Build(ctx, module, fp2); err == nil && b != nil && b.State == delivery.BuildDone {
out[module] = fp2
continue
}
}
if fp, ok := at.fps[module]; ok {
out[module] = fp
} else if fp, ok := at.ran[module]; ok {
out[module] = fp
}
}
return out
}
// writeComposed works out the state's keys from at and writes those that differ, then deletes what is past keeping.
func (c *Composer) writeComposed(ctx context.Context, at *published) (wrote, deleted int, err error) {
values, err := c.composedValues(ctx, at)
if err != nil {
return 0, 0, err
}
c.pubWrite.Lock()
defer c.pubWrite.Unlock()
if c.pubKept == nil {
kept, err := c.env.ComposedState.Entries(ctx)
if err != nil {
return 0, 0, fmt.Errorf("the state cannot be read: %w", err)
}
c.pubKept = kept
}
keys := make([]string, 0, len(values))
for k := range values {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
if bytes.Equal(c.pubKept[key].Value, values[key]) {
continue
}
rev, err := c.env.ComposedState.Put(ctx, key, values[key])
if err != nil {
return wrote, deleted, fmt.Errorf("%s cannot be written: %w", key, err)
}
c.pubKept[key] = ComposedEntry{Value: values[key], Revision: rev}
wrote++
}
for _, key := range c.pastKeeping() {
if err := c.env.ComposedState.Delete(ctx, key); err != nil {
return wrote, deleted, fmt.Errorf("%s cannot be deleted: %w", key, err)
}
delete(c.pubKept, key)
deleted++
}
return wrote, deleted, nil
}
// composedValues is every key the state should hold after the pass at, encoded.
func (c *Composer) composedValues(ctx context.Context, at *published) (map[string][]byte, error) {
r := reader{c: c}
out := map[string][]byte{}
put := func(comp delivery.Composed, plans map[string]catalogue.Plan) error {
if comp.Module != "" && comp.Refused == "" {
if p, ok := plans[comp.Module]; ok {
p := p
comp.Plan = &p
}
}
key, err := composedKeyOf(comp)
if err != nil {
return err
}
raw, err := delivery.EncodeComposed(comp)
if err != nil {
return fmt.Errorf("the composition %s cannot be written: %w", key, err)
}
out[key] = raw
return nil
}
runs := []struct {
scope frombus.Scope
fps map[string]string
comps map[string]delivery.Composed
held map[string]string
}{{at.scope, at.fps, at.comps, at.held}}
// The newest builds, composed only where they differ from the wanted ones.
newest := c.newestFPs(ctx, at)
if !maps.Equal(newest, at.fps) {
scope := at.scope
scope.KeptAt = map[string]string{}
var inOwn []string
c.mu.Lock()
for module, fp := range newest {
if fp != c.modules[module].FP2 {
scope.KeptAt[module] = fp
}
}
c.mu.Unlock()
for _, m := range at.scope.InOwn {
if _, given := newest[m]; !given {
inOwn = append(inOwn, m)
}
}
scope.InOwn = inOwn
comps, _, held, err := frombus.Compose(ctx, r, c.env.Mesh, scope, newest)
switch {
case err != nil && BusFailed(err):
return nil, fmt.Errorf("the newest builds could not be composed: %w", err)
case err != nil:
// A refusal of the node at its newest builds is no refusal of what it applies: the wanted keys are written,
// and the newest are said (each module's have says what holds it when its want moves).
c.sayComposed("this node's newest builds do not compose, so its composition state holds the wanted ones alone: " + err.Error())
default:
for key, comp := range comps {
comps[key] = c.keepRunning(comp, scope.Running)
}
runs = append(runs, struct {
scope frombus.Scope
fps map[string]string
comps map[string]delivery.Composed
held map[string]string
}{scope, newest, comps, held})
}
}
for i, run := range runs {
plans, _, err := frombus.PlansHeld(ctx, r, c.env.Mesh, run.scope)
if err != nil && BusFailed(err) {
return nil, fmt.Errorf("the plans could not be composed: %w", err)
}
// A node whose plans refuse it whole hands the provisioner nothing, as the provisioner's own composing did: no
// key carries a plan then.
for _, comp := range run.comps {
if comp.Module == "" && i > 0 {
continue // the node's own composition is the wanted pass's alone
}
if err := put(comp, plans); err != nil {
return nil, err
}
}
for module, why := range run.held {
fp, ok := run.fps[module]
if !ok || module == agent.NodeModule {
continue
}
if err := put(delivery.Composed{Node: c.env.Node, Module: module, FP: fp, Refused: why}, nil); err != nil {
return nil, err
}
}
}
return out, nil
}
// composedKeyOf is a composition's key in the state.
func composedKeyOf(comp delivery.Composed) (string, error) {
if comp.Module == "" {
return delivery.ComposedNodeKey(comp.Node)
}
return delivery.ComposedKey(comp.Node, comp.Module, comp.FP)
}
// pastKeeping is, of the module keys the state holds, those whose fingerprint is not among the five its module wrote
// last (delivery.ComposedKeep, by revision) and that no want names (ADR 0593 §6). Under pubWrite.
func (c *Composer) pastKeeping() []string {
c.mu.Lock()
wanted := map[string]string{}
for module, w := range c.wants {
wanted[module] = w.FP
}
c.mu.Unlock()
type kept struct {
key string
fp string
rev uint64
}
byModule := map[string][]kept{}
for key, e := range c.pubKept {
_, module, fp, isNode, err := delivery.ParseComposedKey(key)
if err != nil || isNode {
continue
}
byModule[module] = append(byModule[module], kept{key, fp, e.Revision})
}
var out []string
for module, ks := range byModule {
sort.Slice(ks, func(i, j int) bool { return ks[i].rev > ks[j].rev })
for i, k := range ks {
if i < delivery.ComposedKeep || wanted[module] == k.fp {
continue
}
out = append(out, k.key)
}
}
sort.Strings(out)
return out
}
// oweComposed keeps the state due and has the loop make a pass again after the growing bound, as owePackages does.
func (c *Composer) oweComposed() string {
c.mu.Lock()
defer c.mu.Unlock()
c.pubDue = true
c.wait = LaterWait(c.pubTries)
c.pubTries++
c.owed.Store(true)
return fmt.Sprintf("tried again at %s (try %d)", time.Now().Add(c.wait).Format("15:04:05"), c.pubTries)
}
// sayComposed says a line about the state once until it changes; "" clears it.
func (c *Composer) sayComposed(line string) {
c.mu.Lock()
same := c.pubSaid == line
c.pubSaid = line
c.mu.Unlock()
if !same && line != "" {
c.env.Say(line)
}
}
// ranOf is, of what runs, each module's fingerprint by its composition (the node's own left out).
func ranOf(running []delivery.Composed) map[string]string {
out := map[string]string{}
for _, comp := range running {
if comp.Module != "" && comp.FP != "" && !strings.EqualFold(comp.Module, agent.NodeModule) {
out[comp.Module] = comp.FP
}
}
return out
}