View: build_usage does not answer eligibility it cannot read; a resolved capacity key with no readable value is the puller's 0
mesh/delivery merged: source.main moved to novox/mesh-delivery@feef96ed160e
mesh/check checking 4f722899df25 on feef96ed160e
mesh/delivery merged: source.main moved to novox/mesh-delivery@feef96ed160e
mesh/check checking 4f722899df25 on feef96ed160e
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user