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.
This commit is contained in:
2026-10-26 14:46:02 +01:00
parent 7f06ceb960
commit 3e47ab7baf
2 changed files with 88 additions and 32 deletions
@@ -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{} // <node>.<module> -> setting
unknown := "" // why no capacity can be said, when mesh-config's values cannot be read
resolved := map[string]string{} // <node>.<module> -> setting
unknown := "" // why this tool cannot say a capacity: mesh-config's values cannot be read here
unreadable := map[string]string{} // <node>.<module> -> 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))
@@ -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)
}
}