From f947070b7b90430a2a483edd50a2f8919ddeaa51 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:38:24 +0200 Subject: [PATCH] =?UTF-8?q?The=20view=20shows=20the=20controller's=20judge?= =?UTF-8?q?ment=20of=20the=20move=20beside=20each=20want=20(issue=201906?= =?UTF-8?q?=20D7,=20ADR=200539=20=C2=A714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-view/main.go | 2 +- .../mesh-delivery-view/internal/view/serve.go | 10 +++- .../mesh-delivery-view/internal/view/view.go | 58 +++++++++++++------ .../internal/view/wants_test.go | 24 +++++++- modules/mesh-delivery-view/manifest_test.go | 2 +- modules/mesh-delivery-view/module.json | 1 + 6 files changed, 74 insertions(+), 23 deletions(-) diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go index 86a4fc7..0bd581e 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go @@ -139,7 +139,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } model.SetWriter(kvWriter{ctx: ctx, js: js}) model.SetBacklog(view.BacklogOver(js, 5*time.Second)) - for _, bucket := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket} { + for _, bucket := range []string{delivery.HaveBucket, view.AssignmentsBucket, view.BuildsBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.ControllerComposedBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket} { go follow(ctx, js, bucket, model, logger) } select { diff --git a/modules/mesh-delivery-view/internal/view/serve.go b/modules/mesh-delivery-view/internal/view/serve.go index d5e5143..f575a51 100644 --- a/modules/mesh-delivery-view/internal/view/serve.go +++ b/modules/mesh-delivery-view/internal/view/serve.go @@ -225,7 +225,13 @@ func wantText(s Snapshot) [][]string { if w.Rule != 0 { words = fmt.Sprintf("rule %d: %s", w.Rule, w.Words) } - out = append(out, []string{w.Node, w.Module, want, have, w.State, words}) + judged := w.Judgement + if judged == "" { + judged = "none" + } else if w.Judged != "" { + words = strings.TrimSpace(words + " " + w.Judged) + } + out = append(out, []string{w.Node, w.Module, want, have, w.State, judged, words}) } return out } @@ -257,7 +263,7 @@ func backlogText(s Snapshot) [][]string { var backlogHead = []string{"LANGUAGE", "KIND", "WAITING", "HELD", "OLDEST WAITING"} -var wantHead = []string{"NODE", "MODULE", "WANT", "HAVE", "STATE", "WORDS"} +var wantHead = []string{"NODE", "MODULE", "WANT", "HAVE", "STATE", "MOVE JUDGED", "WORDS"} var head = []string{"NODE", "MODULE", "ASSIGNED", "HAVE", "PHASE", "HEALTH", "SINCE", "DETAIL", "WAITS"} diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index 0af41f2..5de9b6f 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -100,6 +100,10 @@ type WantRow struct { Newest string `json:"newest,omitempty"` Rule int `json:"rule,omitempty"` Words string `json:"words,omitempty"` + // Judgement is the controller's judgement of the move to the composition the want names (recreates, moves-data, neither or + // cannot-judge), and Judged its words; empty when the controller wrote none (ADR 0539 §14). + Judgement string `json:"judgement,omitempty"` + Judged string `json:"judged,omitempty"` } // The states of a source revision (ADR 0429 §1). @@ -206,23 +210,24 @@ type UnreadableRow struct { // Model holds the rows' inputs in memory and nothing else: a restart fills it again from the buckets' last values. type Model struct { - mu sync.Mutex - assigns map[string]assignment - builds map[string]buildEntry // by .build. - groups map[string]groupEntry // the indexer's candidates, by group - modules map[string]moduleEntry // the indexer's modules, by module - keys map[string]keyEntry // the builder's build keys, by . - asks map[string]askRecord // the gate's asks, by key - wants map[string]wantEntry // the assigner's wants, by . - source *sourceEntry // the gate's source key - aasks map[string]assignerAskRecord // the assigner's asks (hold, restore), by key - chosen map[string]delivery.ViewAnswer // the answers the view wrote, by ask key - writer Writer - backlog *Backlog - answers map[string]string // by key: the router id of the asking the teller's answer is for ("" when unreadable) - newest map[string]string // module -> id - haves map[string]haveEntry - nodes map[string]nodeEntry + mu sync.Mutex + assigns map[string]assignment + builds map[string]buildEntry // by .build. + groups map[string]groupEntry // the indexer's candidates, by group + modules map[string]moduleEntry // the indexer's modules, by module + keys map[string]keyEntry // the builder's build keys, by . + asks map[string]askRecord // the gate's asks, by key + wants map[string]wantEntry // the assigner's wants, by . + source *sourceEntry // the gate's source key + composed map[string]composedEntry // the controller's digests and judgements, by .. + aasks map[string]assignerAskRecord // the assigner's asks (hold, restore), by key + chosen map[string]delivery.ViewAnswer // the answers the view wrote, by ask key + writer Writer + backlog *Backlog + answers map[string]string // by key: the router id of the asking the teller's answer is for ("" when unreadable) + newest map[string]string // module -> id + haves map[string]haveEntry + nodes map[string]nodeEntry // the watchman's buckets, by key presence map[string]presenceEntry stuck map[string]stuckEntry @@ -290,6 +295,11 @@ type wantEntry struct { err string } +type composedEntry struct { + value delivery.ComposedDigest + err string +} + type sourceEntry struct { revision uint64 err string @@ -324,7 +334,7 @@ type nodeEntry struct { // New is an empty model. func New() *Model { - return &Model{assigns: map[string]assignment{}, builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]delivery.ViewAnswer{}, answers: map[string]string{}, newest: map[string]string{}, + return &Model{assigns: map[string]assignment{}, builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, composed: map[string]composedEntry{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]delivery.ViewAnswer{}, answers: map[string]string{}, newest: map[string]string{}, haves: map[string]haveEntry{}, nodes: map[string]nodeEntry{}, presence: map[string]presenceEntry{}, stuck: map[string]stuckEntry{}} } @@ -431,6 +441,13 @@ func (m *Model) Apply(e Entry) { } w, err := delivery.DecodeWant(e.Value) m.wants[key] = wantEntry{value: w, err: errText(err)} + case delivery.ControllerComposedBucket: + if e.Deleted { + delete(m.composed, e.Key) + return + } + d, err := delivery.DecodeComposedDigest(e.Value) + m.composed[e.Key] = composedEntry{value: d, err: errText(err)} case delivery.SourceBucket: if e.Key != delivery.SourceMainKey { return @@ -903,6 +920,11 @@ func (m *Model) wantRows() []WantRow { if has && h.err == "" { row.Have, row.Phase, row.Health = h.have.FP, string(h.have.Phase), string(h.have.Health) } + if ckey, err := delivery.ComposedKey(node, module, v.FP); err == nil && !v.Removed { + if c, ok := m.composed[ckey]; ok && c.err == "" { + row.Judgement, row.Judged = string(c.value.Judgement), c.value.Words + } + } switch { case v.Removed: row.State = WantRemoved diff --git a/modules/mesh-delivery-view/internal/view/wants_test.go b/modules/mesh-delivery-view/internal/view/wants_test.go index 70a1dfa..d55656b 100644 --- a/modules/mesh-delivery-view/internal/view/wants_test.go +++ b/modules/mesh-delivery-view/internal/view/wants_test.go @@ -130,7 +130,29 @@ func TestTheWantsPageSaysWantsAgainstHaves(t *testing.T) { m := New() fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpOld, delivery.HealthOK)) text := wantText(m.Snapshot()) - if len(text) != 1 || text[0][0] != "ace" || text[0][1] != "web" || text[0][2] != fpNew || text[0][3] != fpOld || text[0][4] != WantPending { + if len(text) != 1 || text[0][0] != "ace" || text[0][1] != "web" || text[0][2] != fpNew || text[0][3] != fpOld || text[0][4] != WantPending || text[0][5] != "none" { + t.Errorf("text = %v", text) + } +} + +// 1906/D7 (ADR 0539 §14): the controller's judgement of the move a want names is shown beside it, with its words. +func TestTheJudgementOfTheMoveIsShownBesideTheWant(t *testing.T) { + m := New() + key, err := delivery.ComposedKey("ace", "web", fpNew) + if err != nil { + t.Fatal(err) + } + v, err := delivery.EncodeComposedDigest(delivery.ComposedDigest{Digest: "sha256:" + strings.Repeat("c", 64), Judgement: delivery.JudgedCannot, Words: "the composition it runs is gone"}) + if err != nil { + t.Fatal(err) + } + fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), Entry{Bucket: delivery.ControllerComposedBucket, Key: key, Value: v, At: t0}) + rows := m.Snapshot().Wants + if len(rows) != 1 || rows[0].Judgement != string(delivery.JudgedCannot) || !strings.Contains(rows[0].Judged, "gone") { + t.Fatalf("rows = %+v", rows) + } + text := wantText(m.Snapshot()) + if len(text[0]) != 7 || text[0][5] != "cannot-judge" { t.Errorf("text = %v", text) } } diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 0344708..7a73bb9 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -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-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "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-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "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) { diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index b0d9e65..c84d39a 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -21,6 +21,7 @@ "reads": [ "mesh-controller.assignments", "mesh-controller.builds", + "mesh-controller.composed", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.wants",