CAT-RD5: mesh-provisioner asks the assigner's placement and wants and the indexer's modules and manifests once and follows their events, no reads

This commit is contained in:
2026-11-04 12:11:18 +01:00
parent 4d05e0d290
commit f49b6de79b
3 changed files with 110 additions and 11 deletions
@@ -8,15 +8,20 @@ package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"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"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-delivery/sdk/views"
)
// Seat is the role this module holds, once for the mesh, and Verb its one verb.
@@ -169,9 +174,73 @@ func slowest(took []Took) time.Duration {
return time.Duration(took[len(took)-1].MS) * time.Millisecond
}
// The owners' read tools of the states the provisioner follows, which the manifest invokes (ADR 0615).
const (
PlacementTool = delivery.AssignerModule + ".assigner_read_placement"
WantsTool = delivery.AssignerModule + ".assigner_read_wants"
ModulesTool = delivery.IndexerModule + ".indexer_read_modules"
ManifestsTool = delivery.IndexerModule + ".indexer_read_manifests"
)
// viewFindings writes the provisioner's finding of a first view unanswered into its own state findings, which the
// condition keeper raises the condition from.
type viewFindings struct{}
func (viewFindings) Put(key string, value []byte) error {
_, err := stdio.State(health.FindingsState).Put(key, json.RawMessage(value))
return err
}
func (viewFindings) Delete(key string) error { return stdio.State(health.FindingsState).Delete(key) }
// newViews is the provisioner's pictures of the assigner's placement and wants and the indexer's modules and manifests:
// each asked once of its owner's read tool, then followed by the owner's events (ADR 0615).
func newViews() *views.Reader {
v := &views.Reader{Module: Module, Logf: logf, Findings: viewFindings{}}
v.Follow(delivery.AssignerModule, "placement", PlacementTool)
v.Follow(delivery.AssignerModule, "wants", WantsTool)
v.Follow(delivery.IndexerModule, "modules", ModulesTool)
v.Follow(delivery.IndexerModule, "manifests", ManifestsTool)
return v
}
// watchedOver is the copy's watch of a state: a followed state is handed from its picture (the first view's values as
// the current ones, then each change), any other from the runtime's watch.
func watchedOver(v *views.Reader) func(string, func(stdio.StateChange) error) error {
return func(state string, handle func(stdio.StateChange) error) error {
if !v.Follows(state) {
return stdio.State(state).Watch("", handle)
}
var landed atomic.Bool
err := v.Watch(state, func(key string, value []byte, gone bool) {
op := stdio.OpPut
if gone {
op = stdio.OpDelete
}
if err := handle(stdio.StateChange{State: state, Key: key, Op: op, Value: value, Current: !landed.Load()}); err != nil {
logf("[%s] %s is not taken: %v", Module, key, err)
}
})
landed.Store(true)
return err
}
}
func main() {
s := runtimeStore{}
ws := stdio.NewWatchedStates() // one copy of what it reads, each state by its watch (ADR 0580 clause 2)
v := newViews()
// One copy of what it reads, each state by its watch (ADR 0580 clause 2); the indexer's and the assigner's by their
// pictures, a manifest by its digest from the picture too.
ws := stdio.WatchedStatesOver(watchedOver(v), func(state, key string) ([]byte, bool, error) {
if v.Follows(state) {
return v.Get(state, key)
}
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
})
v.Start(context.Background())
h := &Holder{Store: Watching(s, ws), Asks: runtimeAsks{}, Now: time.Now, Logf: logf}
h.Compose = FromCompositions(h.Store) // the node-engines' published compositions, from the copy (ADR 0593 §8)
w := &work{pending: map[string]bool{}, wake: make(chan struct{}, 1)}
@@ -234,7 +303,7 @@ func main() {
time.Sleep(wait)
}
}()
if err := stdio.Serve("", append(Tools(s), holderTools(h)...)); err != nil {
if err := stdio.Serve("", append(append(Tools(s), holderTools(h)...), v.Tools("provisioner")...)); err != nil {
logf("%v", err)
os.Exit(1)
}
@@ -8,6 +8,9 @@ import (
"strings"
"sync"
"testing"
"git.novox.be/novox/mesh-delivery/sdk/views"
"git.novox.be/novox/mesh-sdk/go/health"
)
// memStore is the bus as mesh-provisioner reaches it, in memory: the states it keeps.
@@ -289,6 +292,8 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
} `json:"claims"`
State []string `json:"state"`
Reads []string `json:"reads"`
Invokes []string `json:"invokes"`
Consumes []string `json:"consumes"`
Tools []string `json:"tools"`
OwnSecrets map[string]string `json:"own-secrets"`
}
@@ -303,13 +308,13 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
t.Errorf("the claim: %+v", m.Claims)
}
// needs stays declared this release so its first start can empty it (ADR 0570 clause 1).
if want := []string{Registry, Needs, Applied, Collect, ProvisionsState, PlansState, RefusedState}; !slices.Equal(m.State, want) {
if want := []string{Registry, Needs, Applied, Collect, ProvisionsState, PlansState, RefusedState, health.FindingsState}; !slices.Equal(m.State, want) {
t.Errorf("the state is %v, want %v", m.State, want)
}
// Every state and act has its tool (ADR 0570 Consequences): the verb reads the clients' states, the holder's tools
// its own and the composition it does on a change.
served := map[string]bool{}
for _, tool := range append(Tools(newStore()), holderTools(&Holder{})...) {
for _, tool := range append(append(Tools(newStore()), holderTools(&Holder{})...), (&views.Reader{}).Tools("provisioner")...) {
served[tool.Name] = true
}
for what, tool := range map[string]string{Registry: Seat + "." + Verb, Needs: Seat + "." + Verb,
@@ -326,11 +331,25 @@ func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
}
// The builds read serves only haveOutcome's offer check (ADR 0603 clause 5: which manifest the build a provider runs
// was made from) and makes no plan, so ADR 0593 §8 ("it composes nothing") holds.
for _, r := range append(slices.Clone(WatchedReads), ManifestsRead, BuildsRead) {
// The assigner's placement and wants and the indexer's modules and manifests are asked once of their owners' read
// tools and followed by their events, not read (ADR 0615 §1).
followed := []string{PlacementRead, WantsRead, ModulesRead, ManifestsRead}
for _, r := range append(slices.Clone(WatchedReads), BuildsRead) {
if slices.Contains(followed, r) {
if slices.Contains(m.Reads, r) {
t.Errorf("the manifest reads %s, which the holder follows by its owner's events", r)
}
continue
}
if !slices.Contains(m.Reads, r) {
t.Errorf("the manifest does not read %s, which the holder reads", r)
}
}
if !slices.Equal(m.Invokes, []string{PlacementTool, WantsTool, ModulesTool, ManifestsTool}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests"}) {
t.Errorf("invokes %v, consumes %v", m.Invokes, m.Consumes)
}
// It composes nothing (ADR 0593 §8): no config value is read to make a plan.
for _, r := range []string{"mesh-config.resolved"} {
if slices.Contains(m.Reads, r) {
+17 -6
View File
@@ -15,10 +15,6 @@
"operator-channel"
],
"reads": [
"mesh-delivery-assigner.placement",
"mesh-delivery-assigner.wants",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests",
"node-engine.have",
"mesh-delivery-builder.builds",
"mesh-host.composed",
@@ -30,12 +26,26 @@
"node-tools.bound",
"mesh-vault.bound"
],
"invokes": [
"mesh-delivery-assigner.assigner_read_placement",
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_manifests"
],
"consumes": [
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.manifests"
],
"tools": [
"provisioner_provisions",
"provisioner_plans",
"provisioner_compose",
"provisioner_decide",
"provisioner_health"
"provisioner_health",
"provisioner_first_views",
"provisioner_first_view_again"
],
"state": [
"registry",
@@ -44,7 +54,8 @@
"collect",
"provisions",
"plans",
"refused"
"refused",
"findings"
],
"core-credentials": true,
"own-secrets": {