Read the assigner's wants, differences and the haves with tools, so the gap between wants and sends can be read live (issue 1906 D1)
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"mesh-delivery-view/internal/view"
|
||||
@@ -82,9 +83,50 @@ func Tools(r view.Reader) []stdio.Tool {
|
||||
commit, _ := args["commit"].(string)
|
||||
return r.BuilderBuild(module, fp2, commit)
|
||||
}},
|
||||
{Name: view.WantsReadTool,
|
||||
Description: "The assigner's wants and holds as stored in mesh-delivery-assigner_wants: wants by key (want.<node>.<module>) and " +
|
||||
"held fingerprints by key (held.<module>.<hex>). Narrow by node and module (a hold has no node and is shown when no node is given). Read-only.",
|
||||
Input: map[string]any{"type": "object", "properties": nodeModule},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
node, module := nodeModuleOf(args)
|
||||
return r.Wants(node, module)
|
||||
}},
|
||||
{Name: view.WantReadTool,
|
||||
Description: "One want of the assigner as stored: what the node is to run of the module (fingerprint, the newest it is held back from and the rule, removed, words). Read-only.",
|
||||
Input: map[string]any{"type": "object", "required": []string{"node", "module"}, "properties": nodeModule},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
node, module := nodeModuleOf(args)
|
||||
return r.Want(node, module)
|
||||
}},
|
||||
{Name: view.DifferencesReadTool,
|
||||
Description: "The assigner's differences between a want and what the controller sends, as stored in mesh-delivery-assigner_differences, " +
|
||||
"by key (<node>.<module>), with the rule that explains each or unexplained. Narrow by node and module. Read-only.",
|
||||
Input: map[string]any{"type": "object", "properties": nodeModule},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
node, module := nodeModuleOf(args)
|
||||
return r.Differences(node, module)
|
||||
}},
|
||||
{Name: view.HavesReadTool,
|
||||
Description: "What each node has, as the node-engine stored it in node-engine_have: by key <node>.<module> (the fingerprint it runs, its phase and health), " +
|
||||
"and <node>.node (the node's own facts). Narrow by node and module. Read-only.",
|
||||
Input: map[string]any{"type": "object", "properties": nodeModule},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
node, module := nodeModuleOf(args)
|
||||
return r.Haves(node, module)
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
var nodeModule = map[string]any{
|
||||
"node": map[string]any{"type": "string", "description": "only this node's keys"},
|
||||
"module": map[string]any{"type": "string", "description": "only this module's keys"}}
|
||||
|
||||
func nodeModuleOf(args map[string]any) (string, string) {
|
||||
node, _ := args["node"].(string)
|
||||
module, _ := args["module"].(string)
|
||||
return strings.TrimSpace(node), strings.TrimSpace(module)
|
||||
}
|
||||
|
||||
func read(state stdio.KeptState) view.StateRead {
|
||||
return view.StateRead{Keys: state.Keys, GetEntry: func(key string) (json.RawMessage, uint64, error) {
|
||||
e, err := state.Get(key)
|
||||
@@ -102,7 +144,8 @@ func read(state stdio.KeptState) view.StateRead {
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := view.Reader{State: read(stdio.State("mesh-delivery-indexer.modules")), Builds: read(stdio.State("mesh-delivery-builder.builds")), CandidatesState: read(stdio.State("mesh-delivery-indexer.candidates"))}
|
||||
r := view.Reader{State: read(stdio.State("mesh-delivery-indexer.modules")), Builds: read(stdio.State("mesh-delivery-builder.builds")), CandidatesState: read(stdio.State("mesh-delivery-indexer.candidates")),
|
||||
WantsState: read(stdio.State("mesh-delivery-assigner.wants")), DifferencesState: read(stdio.State("mesh-delivery-assigner.differences")), HavesState: read(stdio.State("node-engine.have"))}
|
||||
if err := stdio.Serve("", Tools(r)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -264,3 +264,68 @@ func TestCandidateToolsAnswerTheStoredValue(t *testing.T) {
|
||||
t.Errorf("by module = %v", c)
|
||||
}
|
||||
}
|
||||
|
||||
// 1906/D1 AC1: assigner_wants, assigner_want, assigner_differences and node_haves answer as stored, narrowed by node and module.
|
||||
func keptReader() view.Reader {
|
||||
st := func(f fake) view.StateRead { return f.reader().State }
|
||||
return view.Reader{
|
||||
WantsState: st(fake{
|
||||
"want.ace.zsh": `{"fp":"` + fp2A + `"}`,
|
||||
"want.novox.zsh": `{"fp":"` + fp2B + `"}`,
|
||||
"want.ace.netcheck": `{"fp":"` + fp2A + `","rule":3}`,
|
||||
"held." + keyB: `{"node":"ace","words":"failed"}`,
|
||||
}),
|
||||
DifferencesState: st(fake{"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`}),
|
||||
HavesState: st(fake{"ace.zsh": `{"fp":"x"}`, "ace.node": `{"facts":1}`, "novox.zsh": `{"fp":"y"}`}),
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignerWantsToolAnswersAsStoredAndNarrows(t *testing.T) {
|
||||
r := keptReader()
|
||||
out, err := run(t, r, view.WantsReadTool, map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := out.(view.WantsAnswer)
|
||||
if len(a.Wants) != 3 || len(a.Held) != 1 || string(a.Wants["want.ace.zsh"]) != `{"fp":"`+fp2A+`"}` {
|
||||
t.Errorf("all = %+v", a)
|
||||
}
|
||||
out, _ = run(t, r, view.WantsReadTool, map[string]any{"node": "ace"})
|
||||
if got := out.(view.WantsAnswer).Wants; len(got) != 2 {
|
||||
t.Errorf("node ace = %v", got)
|
||||
}
|
||||
out, _ = run(t, r, view.WantsReadTool, map[string]any{"module": "zsh"})
|
||||
if got := out.(view.WantsAnswer).Wants; len(got) != 2 {
|
||||
t.Errorf("module zsh = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignerWantToolAnswersOneAndSaysWhenThereIsNone(t *testing.T) {
|
||||
r := keptReader()
|
||||
out, err := run(t, r, view.WantReadTool, map[string]any{"node": "ace", "module": "zsh"})
|
||||
if err != nil || string(out.(json.RawMessage)) != `{"fp":"`+fp2A+`"}` {
|
||||
t.Errorf("want = %v %v", out, err)
|
||||
}
|
||||
if _, err := run(t, r, view.WantReadTool, map[string]any{"node": "ace", "module": "gone"}); err == nil || !strings.Contains(err.Error(), "no want") {
|
||||
t.Errorf("a missing want: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignerDifferencesAndNodeHavesToolsAnswerAsStored(t *testing.T) {
|
||||
r := keptReader()
|
||||
out, err := run(t, r, view.DifferencesReadTool, map[string]any{"node": "novox"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := out.(view.KeptAnswer).Keys; len(got) != 1 || string(got["novox.zsh"]) != `{"rule":3}` {
|
||||
t.Errorf("differences = %v", got)
|
||||
}
|
||||
out, _ = run(t, r, view.HavesReadTool, map[string]any{"module": "zsh"})
|
||||
if got := out.(view.KeptAnswer).Keys; len(got) != 2 {
|
||||
t.Errorf("haves of zsh = %v", got)
|
||||
}
|
||||
out, _ = run(t, r, view.HavesReadTool, map[string]any{"node": "ace"})
|
||||
if got := out.(view.KeptAnswer).Keys; len(got) != 2 || got["ace.node"] == nil {
|
||||
t.Errorf("haves of ace (the node's own key too) = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ type Reader struct {
|
||||
State StateRead // the indexer's modules
|
||||
Builds StateRead // the builder's builds
|
||||
|
||||
CandidatesState StateRead // the indexer's candidates
|
||||
CandidatesState StateRead // the indexer's candidates
|
||||
WantsState StateRead // the assigner's wants and holds
|
||||
DifferencesState StateRead // the assigner's differences
|
||||
HavesState StateRead // the node-engine's haves
|
||||
}
|
||||
|
||||
// Module is the stored value of modules.<module>, exactly as the indexer wrote it.
|
||||
@@ -328,3 +331,124 @@ func (r Reader) BuilderBuild(module, fp2, commit string) (BuilderRow, error) {
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// The read tools of the assigner's wants and differences and of the node-engine's haves (issue 1906, D1): the keys of those
|
||||
// buckets answered as stored, so what the assigner wants, what it found different and what each node has can be read live.
|
||||
// They write nothing.
|
||||
const (
|
||||
WantsReadTool = "assigner_wants"
|
||||
WantReadTool = "assigner_want"
|
||||
DifferencesReadTool = "assigner_differences"
|
||||
HavesReadTool = "node_haves"
|
||||
)
|
||||
|
||||
// WantsAnswer is the answer of assigner_wants: the wants by key (`want.<node>.<module>`) and the holds by key
|
||||
// (`held.<module>.<hex>`), each as stored, and the keys that are neither.
|
||||
type WantsAnswer struct {
|
||||
Wants map[string]json.RawMessage `json:"wants"`
|
||||
Held map[string]json.RawMessage `json:"held"`
|
||||
Unreadable []string `json:"unreadable,omitempty"`
|
||||
}
|
||||
|
||||
// KeptAnswer is the answer of assigner_differences and node_haves: the keys of the bucket as stored, and the keys that
|
||||
// are no `<node>.<module>` key.
|
||||
type KeptAnswer struct {
|
||||
Keys map[string]json.RawMessage `json:"keys"`
|
||||
Unreadable []string `json:"unreadable,omitempty"`
|
||||
}
|
||||
|
||||
func (s StateRead) all(what string) (map[string]json.RawMessage, error) {
|
||||
if s.Keys == nil || s.Get == nil {
|
||||
return nil, fmt.Errorf("%s cannot be read here", what)
|
||||
}
|
||||
keys, err := s.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(keys)
|
||||
out := map[string]json.RawMessage{}
|
||||
for _, k := range keys {
|
||||
raw, err := s.Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(raw) > 0 {
|
||||
out[k] = raw
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Wants is every want and hold of the assigner, narrowed by node and module (a hold has no node and is kept when the module
|
||||
// matches).
|
||||
func (r Reader) Wants(node, module string) (WantsAnswer, error) {
|
||||
all, err := r.WantsState.all("the assigner's wants")
|
||||
if err != nil {
|
||||
return WantsAnswer{}, err
|
||||
}
|
||||
out := WantsAnswer{Wants: map[string]json.RawMessage{}, Held: map[string]json.RawMessage{}}
|
||||
for k, raw := range all {
|
||||
if n, m, err := delivery.ParseWantKey(k); err == nil {
|
||||
if (node == "" || n == node) && (module == "" || m == module) {
|
||||
out.Wants[k] = raw
|
||||
}
|
||||
continue
|
||||
}
|
||||
if m, _, err := delivery.ParseHeldKey(k); err == nil {
|
||||
if node == "" && (module == "" || m == module) {
|
||||
out.Held[k] = raw
|
||||
}
|
||||
continue
|
||||
}
|
||||
out.Unreadable = append(out.Unreadable, k)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Want is the stored want of module on node.
|
||||
func (r Reader) Want(node, module string) (json.RawMessage, error) {
|
||||
key, err := delivery.WantKey(strings.TrimSpace(node), strings.TrimSpace(module))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.WantsState.Get == nil {
|
||||
return nil, fmt.Errorf("the assigner's wants cannot be read here")
|
||||
}
|
||||
raw, err := r.WantsState.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil, fmt.Errorf("the assigner has no want %s", key)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func (r Reader) kept(s StateRead, what, node, module string) (KeptAnswer, error) {
|
||||
all, err := s.all(what)
|
||||
if err != nil {
|
||||
return KeptAnswer{}, err
|
||||
}
|
||||
out := KeptAnswer{Keys: map[string]json.RawMessage{}}
|
||||
for k, raw := range all {
|
||||
n, m, isNode, err := delivery.ParseHaveKey(k)
|
||||
if err != nil {
|
||||
out.Unreadable = append(out.Unreadable, k)
|
||||
continue
|
||||
}
|
||||
if (node == "" || n == node) && (module == "" || (!isNode && m == module)) {
|
||||
out.Keys[k] = raw
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Differences is the assigner's differences as stored, narrowed by node and module.
|
||||
func (r Reader) Differences(node, module string) (KeptAnswer, error) {
|
||||
return r.kept(r.DifferencesState, "the assigner's differences", node, module)
|
||||
}
|
||||
|
||||
// Haves is the node-engine's haves (and each node's own key) as stored, narrowed by node and module.
|
||||
func (r Reader) Haves(node, module string) (KeptAnswer, error) {
|
||||
return r.kept(r.HavesState, "the haves", node, module)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
if m.Module != "mesh-delivery-view" {
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
want := []string{"mesh-controller.assignments", "mesh-controller.builds", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-builder.builds", "mesh-delivery-gate.asks", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "node-engine.have"} // 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
want := []string{"mesh-controller.assignments", "mesh-controller.builds", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-gate.asks", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "node-engine.have"} // 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -110,7 +110,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
}
|
||||
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
|
||||
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
|
||||
if want := []string{"indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "builder_build", "builder_builds"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
if want := []string{"indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "builder_build", "builder_builds", "assigner_wants", "assigner_want", "assigner_differences", "node_haves"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
for _, a := range m.Build.Artifacts {
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
"indexer_candidate",
|
||||
"indexer_candidates",
|
||||
"builder_build",
|
||||
"builder_builds"
|
||||
"builder_builds",
|
||||
"assigner_wants",
|
||||
"assigner_want",
|
||||
"assigner_differences",
|
||||
"node_haves"
|
||||
],
|
||||
"state": [
|
||||
{ "name": "answers", "history": 5 }
|
||||
@@ -19,6 +23,7 @@
|
||||
"mesh-controller.builds",
|
||||
"mesh-delivery-assigner.asks",
|
||||
"mesh-delivery-assigner.differences",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-gate.asks",
|
||||
"mesh-delivery-indexer.candidates",
|
||||
|
||||
Reference in New Issue
Block a user