diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 2652d635..3deac5e8 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -398,14 +398,30 @@ func read(state stdio.KeptState) view.StateRead { // ownersRead is the view's reading of the indexer's and the assigner's states: each through its owner's read tool, asked // by the bundle on each call (ADR 0615 §1, design 25 § 12.2: the view keeps its tools and answers them by asking the // owners), never their buckets. -func ownersRead(ask func(key string, body any) (json.RawMessage, error), catalog *meshcatalogclient.Branches) view.Reader { - // Each owner's read tool as the view's invokes names it. - indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, view.IndexerModule+"."+tool) } - assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, view.AssignerModule+"."+tool) } +func ownersRead(ask func(key string, body any) (json.RawMessage, error), holder func(seat string) (string, bool), catalog *meshcatalogclient.Branches) view.Reader { + // Each owner's read tool as the view's invokes names it, asked of the module that holds the owner's seat when it is asked + // (design 47 G A-210, §428): the indexer holds the seat mesh-catalog, the assigner the seat mesh-wants. + atHolder := func(seat string) func(key string, body any) (json.RawMessage, error) { + return func(key string, body any) (json.RawMessage, error) { + module, ok := holder(seat) + if !ok { + return nil, fmt.Errorf("the holder of %s is not found, so %s is not asked", seat, key) + } + return ask(module+"."+key, body) + } + } + indexer := func(tool string) view.StateRead { return view.OwnerRead(atHolder(view.CatalogSeat), tool) } + assigner := func(tool string) view.StateRead { return view.OwnerRead(atHolder(meshwants.Seat), tool) } return view.Reader{State: catalogRead(catalog), CandidatesState: indexer("indexer_read_candidates"), WantsState: assigner("assigner_read_wants"), DifferencesState: assigner("assigner_read_differences"), // The wants' real bucket at the module the tools program reads them of (A-300), as the watchman's stuck keys name it. - WantsHeld: view.AssignerModule + "_" + meshwants.StateWants, + WantsHeld: func() string { + module, ok := holder(meshwants.Seat) + if !ok { + return "" + } + return module + "_" + meshwants.StateWants + }, HeardState: indexer("indexer_read_heard"), PredefinedState: indexer("indexer_read_predefined"), ConfigCandidatesState: indexer("indexer_read_config_candidates"), @@ -449,7 +465,7 @@ func main() { } branches.Logf = logf cat.Start(context.Background()) - r := ownersRead(stdio.Ask, branches) + r := ownersRead(stdio.Ask, holderOf(stdio.Ask), branches) r.Builds, r.HavesState = read(stdio.State("mesh-delivery-builder.builds")), read(stdio.State("node-engine.have")) r.ReachState = read(stdio.State("reach")) r.WaitsState = read(stdio.State(viewstate.WaitsState)) @@ -460,7 +476,7 @@ func main() { r.Assignment, r.Difference = view.AssignmentOf(stdio.Ask), view.DifferenceOf(stdio.Ask) r.ProvisionsState = read(stdio.State(view.ProvisionsRead)) r.BuildLog = stdio.Log(builds.BuildLogRead).Since - r.Pending = pendingOf + r.Pending = pendingOf(holderOf(stdio.Ask)) // The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat // list names for it; the seat list and its two tools are wired once by mesh-sdk (§204). // Its callers name seats, never a module, resolved from the seat-holders view the catalog half's claimant serves (A-295, A-298). @@ -501,11 +517,18 @@ func AnswersTool(callers stdio.Callers, state func(string) views.Source, now fun callers, state(viewstate.ViewAnswersState), state(views.MarksState), now) } -// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5). -func pendingOf() (takein.PendingAnswer, error) { - raw, err := stdio.Ask(view.IndexerModule+".indexer_pending", map[string]any{}) - if err != nil { - return takein.PendingAnswer{}, err +// pendingOf asks the indexer's tool indexer_pending (declared in invokes) of the module holding the seat mesh-catalog: what it +// holds back and why (ADR 0633 §5; design 47 G A-210). +func pendingOf(holder func(seat string) (string, bool)) func() (takein.PendingAnswer, error) { + return func() (takein.PendingAnswer, error) { + module, ok := holder(view.CatalogSeat) + if !ok { + return takein.PendingAnswer{}, fmt.Errorf("the holder of %s is not found", view.CatalogSeat) + } + raw, err := stdio.Ask(module+".indexer_pending", map[string]any{}) + if err != nil { + return takein.PendingAnswer{}, err + } + return takein.DecodePendingAnswer(raw) } - return takein.DecodePendingAnswer(raw) } diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go index b41eb316..34e7efcc 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go @@ -13,11 +13,27 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-sdk/go/views" ) +// assignerModule is the module the tests run the assigner as, as the seat list names it; no code of the view names it. +const assignerModule = "mesh-delivery-assigner" + +// holders answers the seat list as a mesh whose indexer and assigner are not the modules the view once named: the module that +// holds the seat mesh-catalog and the one that holds mesh-wants, each under a name no code of the view knows (design 47 G A-210). +func holders(seat string) (string, bool) { + switch seat { + case meshcatalog.Seat: + return "moved-" + view.IndexerModule, true + case meshwants.Seat: + return "moved-" + assignerModule, true + } + return "", false +} + // source is a fake owner state as a views.Source: what the owner's read tool answers from. type source map[string]string @@ -57,9 +73,9 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { } states := map[string]source{ view.IndexerModule + ".candidates": {"feat/x": cand, "fix": `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`}, - view.AssignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`, + assignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`, "held." + keyB: `{"node":"ace","words":"failed"}`}, - view.AssignerModule + ".differences": {"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`}, + assignerModule + ".differences": {"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`}, view.IndexerModule + "." + view.IndexerPredefinedState: {view.PredefinedListKey: `{"v":1,"keys":["dir:state"]}`}, view.IndexerModule + "." + view.IndexerConfigCandidatesState: {"novox.r.7": `{"v":1,"pull":"novox/r#7"}`}, view.IndexerModule + "." + view.PackagesState: {pkgKey: `{"v":1,"module":"git.novox.be/novox/mesh-sdk/go","commit":"c1"}`}, @@ -71,13 +87,13 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { const heard1 = `{"v":1,"subject":"s","sequence":41,"repository":"novox/mesh-catalog","commit":"9cdac09a"}` states[view.IndexerModule+".heard"] = source{"forge": heard1} histories := map[string]map[string][]views.Revision{ - view.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}}, - view.AssignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}}, + view.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}}, + assignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}}, } // The direct reading, as the tools read the states before (the reads the manifest no longer has). st := func(name string) view.StateRead { return fake(states[name]).reader().State } direct := view.Reader{CandidatesState: st(view.IndexerModule + ".candidates"), - WantsState: st(view.AssignerModule + ".wants"), DifferencesState: st(view.AssignerModule + ".differences"), + WantsState: st(assignerModule + ".wants"), DifferencesState: st(assignerModule + ".differences"), PredefinedState: st(view.IndexerModule + "." + view.IndexerPredefinedState), ConfigCandidatesState: st(view.IndexerModule + "." + view.IndexerConfigCandidatesState), PackagesState: st(view.IndexerModule + "." + view.PackagesState), @@ -86,7 +102,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { tools := map[string]string{} for name := range states { owner, state, _ := strings.Cut(name, ".") - short := map[string]string{view.IndexerModule: "indexer_read_", view.AssignerModule: "assigner_read_"}[owner] + short := map[string]string{view.IndexerModule: "indexer_read_", assignerModule: "assigner_read_"}[owner] tools[owner+"."+short+strings.ReplaceAll(state, "-", "_")] = name } asked := map[string]int{} @@ -95,6 +111,11 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { asked[key]++ return a() } + // The owners are asked at the holders the seat list names, never at the modules the view once named. + if strings.HasPrefix(key, view.IndexerModule+".") || strings.HasPrefix(key, assignerModule+".") { + return nil, fmt.Errorf("%s is asked of a module named in code, not of the seat's holder", key) + } + key = strings.TrimPrefix(key, "moved-") name, ok := tools[key] if !ok { return nil, fmt.Errorf("no tool %s", key) @@ -119,7 +140,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { } return json.Marshal(a) } - owned := ownersRead(ask, startedBranches(t, context.Background(), ask)) + owned := ownersRead(ask, holders, startedBranches(t, context.Background(), ask)) for _, row := range []struct { tool string args map[string]any @@ -174,7 +195,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { away := func(key string, body any) (json.RawMessage, error) { return nil, fmt.Errorf("the holder is away") } gone, cancel := context.WithCancel(context.Background()) cancel() - none := ownersRead(away, startedBranches(t, gone, away)) + none := ownersRead(away, holders, startedBranches(t, gone, away)) if out, err := run(t, none, view.ModulesReadTool, map[string]any{}); err == nil || !strings.Contains(err.Error(), "no watched branch") { t.Errorf("indexer_modules with no view = %v, %v, want an error naming it", out, err) } diff --git a/modules/mesh-delivery-view/internal/view/names.go b/modules/mesh-delivery-view/internal/view/names.go index e0c6704b..d9f15729 100644 --- a/modules/mesh-delivery-view/internal/view/names.go +++ b/modules/mesh-delivery-view/internal/view/names.go @@ -14,8 +14,7 @@ import ( // `consumes`); a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder (novox/hq // ADR 0625 §4, ADR 0634 §1). The indexer's module and its reads are the catalog client's. const ( - IndexerModule = "mesh-delivery-indexer" - AssignerModule = "mesh-delivery-assigner" + IndexerModule = "mesh-delivery-indexer" // ProvisionsRead is the seat mesh-provisioner's state provisions, as the manifest's `reads` names it. ProvisionsRead = "mesh-provisioner.provisions" WatchmanModule = "mesh-delivery-watchman" diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index dbaea5b6..0627b273 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -51,13 +51,13 @@ type Reader struct { State StateRead // the indexer's modules Builds StateRead // the builder's builds - CandidatesState StateRead // the indexer's candidates - WantsState StateRead // the assigner's wants and holds - WantsHeld string // the wants' real bucket at the module WantsState reads, which the watchman's stuck keys name - DifferencesState StateRead // the assigner's differences - HavesState StateRead // the node-engine's haves - ReachState StateRead // the view's own reach (ADR 0556) - WaitsState StateRead // the view's own waits: the parts' wait-held (design 47 G §512, A-242) + CandidatesState StateRead // the indexer's candidates + WantsState StateRead // the assigner's wants and holds + WantsHeld func() string // the wants' real bucket at the module WantsState reads, read at each use, which the watchman's stuck keys name + DifferencesState StateRead // the assigner's differences + HavesState StateRead // the node-engine's haves + ReachState StateRead // the view's own reach (ADR 0556) + WaitsState StateRead // the view's own waits: the parts' wait-held (design 47 G §512, A-242) // HeardState is the FORGE messages the indexer handled (issue 1126 note 76); unset reads none. HeardState StateRead // SignersState is the vault's public signing halves (ADR 0550 §2); unset reads none. diff --git a/modules/mesh-delivery-view/internal/view/status.go b/modules/mesh-delivery-view/internal/view/status.go index 151d35cf..0760efee 100644 --- a/modules/mesh-delivery-view/internal/view/status.go +++ b/modules/mesh-delivery-view/internal/view/status.go @@ -42,9 +42,9 @@ const HealsWithin = 7 * 24 * time.Hour // StatusRead is the five buckets status reads, as a tool reads them. type StatusRead struct { Conditions, Attempts, Presence, Stuck, Wants, Haves StateRead - // WantsHeld is the real bucket of the wants at their holder, which the watchman's stuck keys name (design 47 G A-303); - // "" reads no want as stuck. - WantsHeld string + // WantsHeld answers the real bucket of the wants at their holder, which the watchman's stuck keys name (design 47 G A-303), + // read at each status so a moved holder is named as it moves; "" (or none) reads no want as stuck. + WantsHeld func() string } // Status is the view's answer to status. @@ -157,6 +157,10 @@ func DeriveStatus(r StatusRead, now time.Time) Status { wants, werr := values(r.Wants) haves, herr := values(r.Haves) + held := "" + if r.WantsHeld != nil { + held = r.WantsHeld() + } switch { case werr != nil: st.MovingUnread = "the assigner's wants cannot be read: " + werr.Error() @@ -181,7 +185,7 @@ func DeriveStatus(r StatusRead, now time.Time) Status { } } words, stopped := "", false - if sk, err := meshwatchdogsclient.StuckKey(r.WantsHeld, wk); err == nil && serr == nil && r.WantsHeld != "" { + if sk, err := meshwatchdogsclient.StuckKey(held, wk); err == nil && serr == nil && held != "" { if s, err := meshwatchdogsclient.DecodeStuck(stuck[sk]); err == nil && !s.Moved { words, stopped = s.Waited, true } diff --git a/modules/mesh-delivery-view/internal/view/view_test.go b/modules/mesh-delivery-view/internal/view/view_test.go index 88a93ac3..6824b735 100644 --- a/modules/mesh-delivery-view/internal/view/view_test.go +++ b/modules/mesh-delivery-view/internal/view/view_test.go @@ -310,7 +310,7 @@ func statusDerivesEachPartFromItsBucket(t *testing.T) { "core.doctor.self-check": enc(mcseat.ConditionsByKeyValue{Key: "core.doctor.self-check", Severity: mcc.Urgent, Summary: "no run", Raised: t0.Add(time.Minute)}), }), Presence: states(map[string]string{"a": presence(true), "b": presence(false)}), - WantsHeld: held, + WantsHeld: func() string { return held }, Stuck: states(map[string]string{stuckWant: stuckOf(held, "want.b.web", "b to run web", false), "x.moved": stuckOf("x", "moved", "y", true)}), Wants: states(map[string]string{"want.a.web": want(fp2), "want.a.mail": want(fp1), "want.b.web": want(fp2), "want.a.db": want(fp3)}), diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 4167c0b4..54573bb6 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -258,7 +258,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 t.Errorf("the view follows %s, its manifest consumes %v", family, m.Consumes) } } - for _, tool := range []string{meshcatalogclient.RevisionVerb, meshcatalogclient.RegistrationsVerb, meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants", view.TellerModule + ".teller_read_answers"} { + for _, tool := range []string{meshcatalogclient.RevisionVerb, meshcatalogclient.RegistrationsVerb, meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", "mesh-delivery-assigner.assigner_read_wants", view.TellerModule + ".teller_read_answers"} { if !slices.Contains(verbs(m.Invokes), tool) { t.Errorf("the view asks %s, its manifest invokes %v", tool, m.Invokes) }