From 4f722899df250bdcd51be70dd7d30673875d00ab Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 14:41:35 +0100 Subject: [PATCH] View: build_usage does not answer eligibility it cannot read; a resolved capacity key with no readable value is the puller's 0 A capacity this tool cannot read is unknown to it, not 0: the seat says eligible_known false and the node not known. A key mesh-config holds without a readable value is what the puller reads as 0, and was dropped silently. --- .../mesh-delivery-view/internal/view/usage.go | 87 ++++++++++++++----- .../internal/view/usage_test.go | 33 ++++--- 2 files changed, 88 insertions(+), 32 deletions(-) diff --git a/modules/mesh-delivery-view/internal/view/usage.go b/modules/mesh-delivery-view/internal/view/usage.go index 3216d28..19d43ea 100644 --- a/modules/mesh-delivery-view/internal/view/usage.go +++ b/modules/mesh-delivery-view/internal/view/usage.go @@ -81,8 +81,10 @@ type TryRunning struct { // NodeUsage is one node: each build seat it holds or is set to build for, whether it is eligible to build, and its CPU. type NodeUsage struct { - Node string `json:"node"` - Eligible bool `json:"eligible"` + Node string `json:"node"` + // Eligible is the node's verdict: "eligible" when a seat of it is, "not eligible" when every seat is known not to be, + // and "not known" when none is known eligible and this tool cannot tell for some seat. + Eligible string `json:"eligible"` // Said is one line per seat, the answer in words. Said []string `json:"said"` Seats []SeatUsage `json:"seats"` @@ -101,10 +103,13 @@ type SeatUsage struct { CapacityFrom string `json:"capacity_from"` Paused bool `json:"paused"` // Eligible says it takes asks: it holds the seat, has a capacity above 0 and is not paused; WhyNot says why not. - Eligible bool `json:"eligible"` - WhyNot string `json:"why_not,omitempty"` - Running RunningUsage `json:"running"` - LeftRunning []StepUsage `json:"left_running"` + // EligibleKnown is false when this tool cannot tell (it cannot read mesh-config's capacities): Eligible is then + // absent, never false, and WhyNot starts "not known: ". + Eligible *bool `json:"eligible,omitempty"` + EligibleKnown bool `json:"eligible_known"` + WhyNot string `json:"why_not,omitempty"` + Running RunningUsage `json:"running"` + LeftRunning []StepUsage `json:"left_running"` // EndedStale are the steps of the last day whose end their build module wrote for a key left running (a restart, or // past the step bound): counted here alone, never as finished. EndedStale []StepUsage `json:"ended_stale"` @@ -242,8 +247,9 @@ func (u UsageReader) Usage() UsageAnswer { } // Capacity: mesh-config's resolved value of the module's `capacity` on each node. - resolved := map[string]string{} // . -> setting - unknown := "" // why no capacity can be said, when mesh-config's values cannot be read + resolved := map[string]string{} // . -> setting + unknown := "" // why this tool cannot say a capacity: mesh-config's values cannot be read here + unreadable := map[string]string{} // . -> why its resolved key holds no readable value: the puller uses 0 if u.Resolved.Keys == nil || u.Resolved.Get == nil { unknown = "mesh-config's resolved values are not readable here" } else if keys, err := u.Resolved.Keys(); err != nil { @@ -260,17 +266,35 @@ func (u UsageReader) Usage() UsageAnswer { unknown = "mesh-config's resolved capacities cannot be read: " + err.Error() } for i, k := range want { - var r struct { - Value json.RawMessage `json:"value"` + if err != nil { + break // the read failed: the values are unknown to this tool, not unreadable in mesh-config } + var r struct { + Value json.RawMessage `json:"value"` + Source string `json:"source"` + } + key := strings.TrimSuffix(k, "."+buildstep.CapacitySetting) setting := "" - if i < len(raws) && json.Unmarshal(raws[i], &r) == nil && json.Unmarshal(r.Value, &setting) == nil { - resolved[strings.TrimSuffix(k, "."+buildstep.CapacitySetting)] = setting + switch { + case i >= len(raws) || len(raws[i]) == 0: + unreadable[key] = "its resolved key holds no value" + case json.Unmarshal(raws[i], &r) != nil: + unreadable[key] = "its resolved key is not a resolved value" + case r.Source == "unreadable": + unreadable[key] = "mesh-config resolves it as unreadable" + case len(r.Value) == 0 || string(r.Value) == "null" || json.Unmarshal(r.Value, &setting) != nil || strings.TrimSpace(setting) == "": + unreadable[key] = fmt.Sprintf("its resolved value %s is empty or not a capacity", strings.TrimSpace(string(r.Value))) + default: + resolved[key] = setting } } } if unknown != "" { - note("capacity unknown: %s, and a puller whose capacity cannot be read uses 0", unknown) + note("capacity unknown to this tool: %s; eligibility is not answered for a seat that holds its module", unknown) + } + for key := range unreadable { + node, module, _ := strings.Cut(key, ".") + seat(node, strings.TrimPrefix(module, "mesh-build-")) } // Every node a capacity names is listed, so "is this node eligible" is answered for any node: the nodes each module's // resolved value names (as resolved on the first node in name order), or the modules' default when mesh-config @@ -411,8 +435,11 @@ func (u UsageReader) Usage() UsageAnswer { // value names for the node. setting, ok = named[lang], true } + bad, isBad := unreadable[node+"."+s.Module] if unknown != "" { - s.CapacityFrom = "capacity unknown: the puller uses 0 (" + unknown + ")" + s.CapacityFrom = "capacity unknown to this tool: " + unknown + } else if isBad { + s.CapacityFrom = "mesh-config's value for " + s.Module + " on " + node + " cannot be read (" + bad + "), so its puller uses 0" } else if ok { s.CapacityFrom = from if caps, err := buildrun.ParseCapacity(setting); err != nil { @@ -426,21 +453,34 @@ func (u UsageReader) Usage() UsageAnswer { s.CapacityFrom = fmt.Sprintf("the modules' default %q: mesh-config holds no resolved value for %s on %s", buildstep.DefaultCapacities, s.Module, node) } s.Paused = paused[node+"."+lang] + s.EligibleKnown = true switch { case !s.Holds && s.Phase == "": s.WhyNot = s.Module + " is not on " + node case !s.Holds: s.WhyNot = s.Module + " on " + node + " is " + s.Phase - case unknown != "": - s.WhyNot = "capacity unknown: the puller uses 0" - case s.Capacity == 0: - s.WhyNot = "its capacity on " + node + " is 0" case s.Paused: s.WhyNot = "paused (" + s.Seat + ".resume takes asks again)" - default: - s.Eligible = true + case unknown != "": + s.EligibleKnown = false + s.WhyNot = "not known: this tool cannot read its capacity (" + unknown + ")" + case isBad: + s.WhyNot = "its capacity on " + node + " cannot be read, so its puller uses 0" + case s.Capacity == 0: + s.WhyNot = "its capacity on " + node + " is 0" + } + if s.EligibleKnown { + e := s.WhyNot == "" + s.Eligible = &e + } + switch { + case s.Eligible != nil && *s.Eligible: + nu.Eligible = "eligible" + case !s.EligibleKnown && nu.Eligible != "eligible": + nu.Eligible = "not known" + case nu.Eligible == "": + nu.Eligible = "not eligible" } - nu.Eligible = nu.Eligible || s.Eligible s.fill(steps[node][lang], now) nu.Said = append(nu.Said, s.said()) nu.Seats = append(nu.Seats, *s) @@ -548,7 +588,10 @@ func (s *SeatUsage) fill(steps []usageStep, now time.Time) { // said is the seat's use in one line. func (s *SeatUsage) said() string { head := s.Seat + ": eligible" - if !s.Eligible { + switch { + case !s.EligibleKnown: + head = s.Seat + ": eligibility " + s.WhyNot + case s.Eligible == nil || !*s.Eligible: head = s.Seat + ": not eligible, " + s.WhyNot } kinds := make([]string, 0, len(s.Running.ByKind)) diff --git a/modules/mesh-delivery-view/internal/view/usage_test.go b/modules/mesh-delivery-view/internal/view/usage_test.go index 24d43d2..580018a 100644 --- a/modules/mesh-delivery-view/internal/view/usage_test.go +++ b/modules/mesh-delivery-view/internal/view/usage_test.go @@ -72,12 +72,13 @@ func usageFixture() UsageReader { return UsageReader{ Now: func() time.Time { return usageNow }, Haves: states(map[string]string{"shanks.mesh-build-go": running, "shanks.mesh-build-node": running, "ace.mesh-build-go": running, - "g14.mesh-build-go": running, "shanks.node": `{}`, "shanks.mesh-delivery-view": running}), + "g14.mesh-build-go": running, "g14.mesh-build-node": running, "shanks.node": `{}`, "shanks.mesh-delivery-view": running}), Resolved: states(map[string]string{ "shanks.mesh-build-go.capacity": `{"node":"shanks","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "ace.mesh-build-go.capacity": `{"node":"ace","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "g14.mesh-build-go.capacity": `{"node":"g14","module":"mesh-build-go","key":"capacity","value":"shanks=2,ace=2,g14=0,novox=0","source":"mesh"}`, "shanks.mesh-config.other": `{"value":"x"}`, + "g14.mesh-build-node.capacity": `{"node":"g14","module":"mesh-build-node","key":"capacity","source":"unreadable"}`, }), Steps: map[string]StateRead{"go": states(goSteps), "node": states(map[string]string{}), "image": states(map[string]string{})}, Pauses: map[string]StateRead{"go": states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`}), "node": states(nil), "image": states(nil)}, @@ -113,7 +114,7 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { t.Logf("build_usage answers:\n%s", out) } sg := seatOf(t, a, "shanks", "node-build-go") - eligible := map[string]bool{} + eligible := map[string]string{} for _, n := range a.Nodes { eligible[n.Node] = n.Eligible } @@ -122,7 +123,9 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { got, want any }{ {"nodes, novox named by the default capacity", len(a.Nodes), 4}, - {"eligible", eligible, map[string]bool{"shanks": true, "ace": false, "g14": false, "novox": false}}, + {"eligible", eligible, map[string]string{"shanks": "eligible", "ace": "not eligible", "g14": "not eligible", "novox": "not eligible"}}, + {"a resolved key with no readable value: the puller uses 0", seatOf(t, a, "g14", "node-build-node").WhyNot, "its capacity on g14 cannot be read, so its puller uses 0"}, + {"and is said so", seatOf(t, a, "g14", "node-build-node").CapacityFrom, "mesh-config's value for mesh-build-node on g14 cannot be read (mesh-config resolves it as unreadable), so its puller uses 0"}, {"novox why not", seatOf(t, a, "novox", "node-build-go").WhyNot, "mesh-build-go is not on novox"}, {"novox capacity, from the module's value resolved elsewhere", seatOf(t, a, "novox", "node-build-go").CapacityFrom, "mesh-config, resolved for mesh-build-go on ace (mesh-build-go is not assigned to novox)"}, {"ended by restart or bound, apart", len(sg.EndedStale), 2}, @@ -163,7 +166,7 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { // Nothing readable: each state is named in a note, the backlog's refusal too, and nothing is said held. none := UsageReader{Now: func() time.Time { return usageNow }, Backlog: func() ([]BacklogRow, error) { return nil, errors.New("no socket") }}.Usage() notes := strings.Join(none.Notes, " | ") - for _, want := range []string{"have is not readable", "capacity unknown: mesh-config's resolved values are not readable", "steps of mesh-build-go are not readable", "tries are not readable", "did not answer: no socket"} { + for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "steps of mesh-build-go are not readable", "tries are not readable", "did not answer: no socket"} { if !strings.Contains(notes, want) { t.Errorf("the notes %q do not say %q", notes, want) } @@ -171,11 +174,21 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { if none.Backlog == nil { t.Errorf("nothing readable answered no backlog list") } - for _, n := range none.Nodes { // the default's nodes, none said eligible on a capacity nobody can read - for _, st := range n.Seats { - if st.Eligible || !strings.HasPrefix(st.CapacityFrom, "capacity unknown: the puller uses 0") { - t.Errorf("with nothing readable %s on %s says eligible %v, capacity from %q", st.Seat, n.Node, st.Eligible, st.CapacityFrom) - } - } + + // mesh-config unreadable to this tool: a seat that holds its module is not answered (never said not eligible), and its + // node's verdict is not known; a seat known not eligible on other grounds (paused) stays so. + u := usageFixture() + u.Resolved = StateRead{} + blind := u.Usage() + sb := seatOf(t, blind, "shanks", "node-build-go") + verdict := map[string]string{} + for _, n := range blind.Nodes { + verdict[n.Node] = n.Eligible + } + if sb.EligibleKnown || sb.Eligible != nil || !strings.HasPrefix(sb.WhyNot, "not known: ") || !strings.HasPrefix(sb.CapacityFrom, "capacity unknown to this tool: ") { + t.Errorf("unknown capacity: known %v eligible %v why %q from %q", sb.EligibleKnown, sb.Eligible, sb.WhyNot, sb.CapacityFrom) + } + if verdict["shanks"] != "not known" || verdict["ace"] != "not eligible" { + t.Errorf("verdicts with mesh-config unreadable = %v, want shanks not known, ace not eligible (paused)", verdict) } }