Files
mesh-host/internal/selfcompose/data.go
T
jschoubben 122f4dd727 Read a done build once, and work out the data items in the scope the pass composed in (ADR 0577)
The data items after a pass re-read each placed module's build over the bus (about 55 per pass on
ace, g14 and shanks); a done build never changes, so the composer keeps it. The items are now
worked out with the pass's own kept builds and modules run inside the node's own, as composed.

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

204 lines
6.3 KiB
Go

package selfcompose
import (
"bytes"
"context"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// **The data items of each module placed here, as this engine composed them** (novox/hq ADR 0577 clause 1). After a
// pass that composed and handed something new (or the first one of a run), the composer works out each placed module's
// data items from the same inputs it composed from (frombus.DataItems, which reads them through this composer's own
// view of the bus) and keeps each module's in the engine's state data, key `<node>.<module>`, with the digest of the
// module's composition: written only when the value differs, deleted when the module no longer declares any here or left
// the node. That node's disk-load watches its node's keys and measures what they name; it composes nothing (clause 2).
// DataStore is this node's keys of the engine's state data (link.EngineData).
type DataStore interface {
// Get is a key's value, nil when it has none.
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, value []byte) error
Delete(ctx context.Context, key string) error
// Keys is every key matching pattern that holds a value.
Keys(ctx context.Context, pattern string) ([]string, error)
}
// itemsReader is the composer's view of the bus for working out data items: no own secret is asked of the vault, since
// working out where an item lies reads no secret (the items are rendered as a plan is, compose.Planning).
type itemsReader struct{ reader }
func (itemsReader) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) {
return nil, nil
}
// dataItemsOf works out the data items of the scope's node (frombus.DataItems); a test sets what it answers.
var dataItemsOf = frombus.DataItems
// publishData keeps the data items of this node's modules in the engine's state data, when a pass asked it (dataDue).
func (c *Composer) publishData(ctx context.Context) {
c.mu.Lock()
due := c.dataDue
c.dataDue = false
c.mu.Unlock()
if !due || c.env.Data == nil {
return
}
c.mu.Lock()
scope := c.dataScope
c.mu.Unlock()
if scope.Node == "" {
scope.Node = c.env.Node
}
items, unplaced, err := dataItemsOf(ctx, itemsReader{reader{c: c}}, c.env.Mesh, scope)
if err != nil {
c.sayData("the data items of this node's modules cannot be worked out, and what the engine's state " +
delivery.EngineDataBucket + " holds stands: " + err.Error())
return
}
if len(unplaced) > 0 {
c.env.Say("data items left out as not placeable: " + strings.Join(unplaced, "; "))
}
c.mu.Lock()
digests := c.digestsByModule()
c.mu.Unlock()
wrote, removed, err := c.writeData(ctx, items, digests)
if err != nil {
c.mu.Lock()
c.dataDue = true // the next pass tries again, whatever it hands
c.mu.Unlock()
c.sayData("the data items of this node's modules are not all kept in " + delivery.EngineDataBucket +
", and are written again at the next composition: " + err.Error())
return
}
c.sayData("")
if wrote+removed > 0 {
c.env.Say(fmt.Sprintf("the data items of this node's modules: %d key(s) of %s written, %d deleted",
wrote, delivery.EngineDataBucket, removed))
}
}
// sayData says a line about the data items once until it changes; "" clears it.
func (c *Composer) sayData(line string) {
c.mu.Lock()
same := c.dataSaid == line
c.dataSaid = line
c.mu.Unlock()
if !same && line != "" {
c.env.Say(line)
}
}
// digestsByModule is the digest of each module's composition handed last, and the node's own under "" for a module run
// inside it. Under c.mu.
func (c *Composer) digestsByModule() map[string]string {
out := map[string]string{}
for key, dig := range c.handed {
_, module, _, isNode, err := delivery.ParseComposedKey(key)
if err != nil {
continue
}
if isNode {
module = ""
}
out[module] = dig
}
return out
}
// writeData writes each module's key whose value differs from what the key holds, and deletes this node's keys of
// modules with no items here now. The keys are read once per run (dataKept), so a restart that finds the same items
// writes nothing.
func (c *Composer) writeData(ctx context.Context, items map[string][]catalogue.PlacedDataItem, digests map[string]string) (wrote, removed int, err error) {
store := c.env.Data
if c.dataKept == nil {
pattern, err := delivery.EngineDataNodeKeys(c.env.Node)
if err != nil {
return 0, 0, err
}
keys, err := store.Keys(ctx, pattern)
if err != nil {
return 0, 0, fmt.Errorf("this node's keys cannot be listed: %w", err)
}
kept := map[string][]byte{}
for _, k := range keys {
raw, err := store.Get(ctx, k)
if err != nil {
return 0, 0, fmt.Errorf("%s cannot be read: %w", k, err)
}
if raw != nil {
kept[k] = raw
}
}
c.dataKept = kept
}
want := map[string][]byte{}
modules := make([]string, 0, len(items))
for m := range items {
modules = append(modules, m)
}
sort.Strings(modules)
var failed []string
for _, module := range modules {
if len(items[module]) == 0 {
continue
}
key, err := delivery.EngineDataKey(c.env.Node, module)
if err != nil {
failed = append(failed, err.Error())
continue
}
digest, ok := digests[module]
if !ok {
digest = digests[""] // run inside the node's own composition
}
raw, err := delivery.EncodeEngineData(delivery.EngineData{Node: c.env.Node, Module: module, Digest: digest, Items: items[module]})
if err != nil {
failed = append(failed, err.Error())
continue
}
want[key] = raw
}
keys := make([]string, 0, len(want))
for k := range want {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
if bytes.Equal(c.dataKept[key], want[key]) {
continue
}
if err := store.Put(ctx, key, want[key]); err != nil {
failed = append(failed, key+": "+err.Error())
continue
}
c.dataKept[key] = want[key]
wrote++
}
var gone []string
for key := range c.dataKept {
if _, still := want[key]; !still {
gone = append(gone, key)
}
}
sort.Strings(gone)
for _, key := range gone {
if err := store.Delete(ctx, key); err != nil {
failed = append(failed, key+": "+err.Error())
continue
}
delete(c.dataKept, key)
removed++
}
if len(failed) > 0 {
return wrote, removed, fmt.Errorf("%s", strings.Join(failed, "; "))
}
return wrote, removed, nil
}