The view shows the controller's judgement of the move beside each want (issue 1906 D7, ADR 0539 §14)

This commit is contained in:
2026-10-24 16:38:24 +02:00
parent 04c2c60f02
commit f947070b7b
6 changed files with 74 additions and 23 deletions
@@ -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 {
@@ -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"}
@@ -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 <module>.build.<id>
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 <module>.<fingerprint>
asks map[string]askRecord // the gate's asks, by key
wants map[string]wantEntry // the assigner's wants, by <node>.<module>
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 <module>.build.<id>
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 <module>.<fingerprint>
asks map[string]askRecord // the gate's asks, by key
wants map[string]wantEntry // the assigner's wants, by <node>.<module>
source *sourceEntry // the gate's source key
composed map[string]composedEntry // the controller's digests and judgements, by <node>.<module>.<hex>
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
@@ -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)
}
}
+1 -1
View File
@@ -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) {
+1
View File
@@ -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",