diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/main.go b/modules/mesh-provisioner/cmd/mesh-provisioner/main.go index 7ea6bfe4..8575bdcc 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/main.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/main.go @@ -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) } diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go index c856a63b..6f85415d 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go @@ -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) { diff --git a/modules/mesh-provisioner/module.json b/modules/mesh-provisioner/module.json index 1c6f4c74..b947478f 100644 --- a/modules/mesh-provisioner/module.json +++ b/modules/mesh-provisioner/module.json @@ -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": {