From 1618837ea9feaee7ef26b0eb0e5dd98975f26b76 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 12:38:26 +0100 Subject: [PATCH] A build seat's puller takes nothing while its module finds its node cannot build, and says why A node whose module proves a need of the machine missing (ace's /dev/net/tun, 2026-10-28) pulled image asks and failed each one; judged ineligible, its capacity is 0 with the reason, as build hours are, so the asks go to a node that can build them (issue 1126 note). --- go/buildstep/capacity_test.go | 30 ++++++++++++++++++++---------- go/buildstep/pull.go | 17 ++++++++++++++++- go/delivery/stepasks.go | 3 +++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/go/buildstep/capacity_test.go b/go/buildstep/capacity_test.go index a851135..6a22aa8 100644 --- a/go/buildstep/capacity_test.go +++ b/go/buildstep/capacity_test.go @@ -86,6 +86,8 @@ func TestNodeCapacityAndLimits(t *testing.T) { // The operator, 2026-10-26 21:18: g14 builds only from 18:00 to 06:00, its local time. Outside its build hours the // capacity judged by is 0, said with the window and when it opens; a node not named, or a bad value, builds at any hour. + // And a node its module proves cannot build (ace without /dev/net/tun, 2026-10-28) judges by 0 at every hour, saying + // why, so its asks go to a node that can. cet := time.FixedZone("CET", 3600) day := func(hh, mm int) time.Time { return time.Date(2026, 10, 26, hh, mm, 0, 0, cet) } was := now @@ -96,15 +98,18 @@ func TestNodeCapacityAndLimits(t *testing.T) { effective int edge string // the window's next edge, HH:MM; "" for no window bad bool + ineligible string // what the module's Eligible says; "" for eligible }{ - {"g14 in the office", "g14=18:00-06:00", "g14", day(10, 0), 0, "18:00", false}, - {"g14 in the evening", "g14=18:00-06:00", "g14", day(18, 0), 2, "06:00", false}, - {"g14 past midnight", "g14=18:00-06:00", "g14", day(5, 59), 2, "06:00", false}, - {"g14 at the close", "g14=18:00-06:00", "g14", day(6, 0), 0, "18:00", false}, - {"a day window", "shanks=09:00-17:30,g14=18:00-06:00", "shanks", day(17, 29), 2, "17:30", false}, - {"a node not named", "g14=18:00-06:00", "ace", day(10, 0), 2, "", false}, - {"a bad time", "g14=18:00-6", "g14", day(10, 0), 2, "", true}, - {"no span", "g14=18:00-18:00", "g14", day(10, 0), 2, "", true}, + {"g14 in the office", "g14=18:00-06:00", "g14", day(10, 0), 0, "18:00", false, ""}, + {"g14 in the evening", "g14=18:00-06:00", "g14", day(18, 0), 2, "06:00", false, ""}, + {"g14 past midnight", "g14=18:00-06:00", "g14", day(5, 59), 2, "06:00", false, ""}, + {"g14 at the close", "g14=18:00-06:00", "g14", day(6, 0), 0, "18:00", false, ""}, + {"a day window", "shanks=09:00-17:30,g14=18:00-06:00", "shanks", day(17, 29), 2, "17:30", false, ""}, + {"a node not named", "g14=18:00-06:00", "ace", day(10, 0), 2, "", false, ""}, + {"a bad time", "g14=18:00-6", "g14", day(10, 0), 2, "", true, ""}, + {"no span", "g14=18:00-18:00", "g14", day(10, 0), 2, "", true, ""}, + {"ace without /dev/net/tun", "", "ace", day(10, 0), 0, "", false, "/dev/net/tun cannot be opened: no such device"}, + {"not eligible within its hours", "g14=18:00-06:00", "g14", day(19, 0), 0, "06:00", false, "/dev/net/tun cannot be opened: no such device"}, } { w, err := ParseBuildHours(c.setting, c.node) if (err != nil) != c.bad { @@ -121,11 +126,16 @@ func TestNodeCapacityAndLimits(t *testing.T) { file := filepath.Join(t.TempDir(), "settings.json") os.WriteFile(file, []byte(`{"build-hours":"`+c.setting+`"}`), 0o600) now = func() time.Time { return c.at } - p := &Puller{Language: "go", Node: c.node, Capacity: func() int { return 2 }, BuildHours: NodeBuildHours(file, c.node, nil)} + p := &Puller{Language: "go", Node: c.node, Capacity: func() int { return 2 }, BuildHours: NodeBuildHours(file, c.node, nil), + Eligible: func() string { return c.ineligible }} got := p.CapacityDetail() - if got.Effective != c.effective || got.OutsideHours != (c.effective == 0) || (c.effective == 0 && !strings.Contains(got.Why, "outside its build hours 18:00-06:00 CET until 18:00")) { + outside := c.effective == 0 && c.ineligible == "" + if got.Effective != c.effective || got.OutsideHours != outside || (outside && !strings.Contains(got.Why, "outside its build hours 18:00-06:00 CET until 18:00")) { t.Errorf("%s: %+v, want capacity %d", c.name, got, c.effective) } + if got.Ineligible != c.ineligible || (c.ineligible != "" && !strings.HasPrefix(got.Why, "0, not eligible: "+c.ineligible+" (otherwise ")) { + t.Errorf("%s: not eligible is not said: %+v", c.name, got) + } // A value that cannot be read builds at any hour, loudly: the record names the value and says why. if c.bad && (!strings.Contains(got.BuildHoursFault, c.setting) || !strings.Contains(got.Why, "at any hour, because its build hours cannot be read")) { t.Errorf("%s: a bad value is not said in the record: %+v", c.name, got) diff --git a/go/buildstep/pull.go b/go/buildstep/pull.go index 893d631..afa68e9 100644 --- a/go/buildstep/pull.go +++ b/go/buildstep/pull.go @@ -86,6 +86,13 @@ type Puller struct { // mesh-config resolves for `build-hours` takes its place once handed. Outside the window the capacity judged by is 0: // steps running finish, no new one is taken; the puller wakes at the window's next edge. Nil builds at any hour. BuildHours func() (*BuildWindow, string) + // Eligible says why this node cannot run the seat's asks at all now, and "" when it can: what the module proves of + // its machine and finds missing (mesh-build-image: /dev/net/tun cannot be opened, so buildah's network for a RUN line + // cannot be made; 2026-10-28 on ace). Read at every judgement, so it is cheap (a module answers its last look). + // While it says a reason the capacity judged by is 0, said with the reason: steps running finish, no new one is + // taken, and the asks go to a node that can run them instead of failing here. The module calls Wake when its answer + // changes. Nil is always eligible. + Eligible func() string // Has says whether this node has a need an ask names (delivery.AskNeeds, issue 890). An ask needing what it lacks is // declined: its step ends refused as declined (delivery.StepDeclined), the message is dropped for its asker to put // again, and the worker it came from is not looked at again until the next pull's wait, so a node lacking the need @@ -291,6 +298,13 @@ func (p *Puller) CapacityDetail() delivery.Capacity { c.Why = fmt.Sprintf("%s; within its build hours %s until %s", c.Why, c.BuildHours, w.NextEdge(at).Format("15:04")) } } + if p.Eligible != nil { + if why := p.Eligible(); why != "" { + c.Ineligible = why + c.Why = fmt.Sprintf("0, not eligible: %s (otherwise %s)", why, c.Why) + c.Effective = 0 + } + } p.sayCapacity(c) return c } @@ -326,7 +340,8 @@ func (p *Puller) sayCapacity(c delivery.Capacity) { p.mu.Lock() same := p.hasJudged && p.judged.Effective == c.Effective && p.judged.Configured == c.Configured && p.judged.ByCPU == c.ByCPU && p.judged.CoresPerStep == c.CoresPerStep && p.judged.QuotaCores == c.QuotaCores && p.judged.OutsideHours == c.OutsideHours && - p.judged.BuildHours == c.BuildHours && p.judged.BuildHoursFault == c.BuildHoursFault + p.judged.BuildHours == c.BuildHours && p.judged.BuildHoursFault == c.BuildHoursFault && + p.judged.Ineligible == c.Ineligible if !same { p.judged, p.hasJudged, p.capWritten = c, true, false } diff --git a/go/delivery/stepasks.go b/go/delivery/stepasks.go index 6b8956a..f940dbf 100644 --- a/go/delivery/stepasks.go +++ b/go/delivery/stepasks.go @@ -730,6 +730,9 @@ type Capacity struct { OutsideHours bool `json:"outside-hours,omitempty"` // BuildHoursFault is why the node's build-hours value cannot be read, naming the value: it then builds at any hour. BuildHoursFault string `json:"build-hours-fault,omitempty"` + // Ineligible is why the module found its node cannot run its asks (buildstep.Puller.Eligible), absent when it can: + // Effective is then 0 at every hour. + Ineligible string `json:"ineligible,omitempty"` // Why is the capacity in words: what bounds it. Why string `json:"why"` At time.Time `json:"at,omitzero"`