From fed64292f0de6968f6588e0d1dc917c951aa9359 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 18:13:18 +0100 Subject: [PATCH] =?UTF-8?q?Read=20build=20capacity=20as=20an=20integer=20a?= =?UTF-8?q?nd=20build=20hours=20as=20one=20window,=20default=200:=20no=20r?= =?UTF-8?q?epository=20names=20a=20node=20(ADR=200567=20=C2=A712,=20issue?= =?UTF-8?q?=201126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/buildstep/buildhours.go | 68 +++++++++++++---------------- go/buildstep/capacity.go | 65 +++++++++++++-------------- go/buildstep/capacity_cpu.go | 4 +- go/buildstep/capacity_test.go | 57 ++++++++++-------------- go/buildstep/phase.go | 2 +- go/buildstep/pull.go | 12 ++--- go/buildstep/pull_capacity_test.go | 4 +- go/buildstep/pull_test.go | 4 +- go/buildstep/stale.go | 2 +- go/delivery/buildrun/places.go | 26 ----------- go/delivery/buildrun/places_test.go | 26 +---------- 11 files changed, 99 insertions(+), 171 deletions(-) diff --git a/go/buildstep/buildhours.go b/go/buildstep/buildhours.go index e0bc991..6e3dee4 100644 --- a/go/buildstep/buildhours.go +++ b/go/buildstep/buildhours.go @@ -8,14 +8,15 @@ import ( "time" ) -// **A node may build only within its build hours** (the operator, 2026-10-26 21:18: "we should only use g14 as a builder -// outside office hours", "from 18 o'clock until 6 in the morning"). A build module's `build-hours` setting names, per -// node, the window in the node's local time in which it takes asks; outside it the capacity judged by is 0: steps running -// finish, no new one is taken. A node it does not name, or an empty setting, builds at any hour. The puller judges it at -// every judgement and wakes on one timer to the window's next edge (a clock edge, not a poll of another part). +// **A node may build only within its build hours** (the operator, 2026-10-26 21:18: the laptop builds only outside +// office hours, "from 18 o'clock until 6 in the morning"). A build module's `build-hours` setting is the node's window +// in its local time, a config value set on each node's layer of mesh-config (ADR 0567 §12: no repository names a node); +// outside it the capacity judged by is 0: steps running finish, no new one is taken. An empty value builds at any hour. +// The puller judges it at every judgement and wakes on one timer to the window's next edge (a clock edge, not a poll of +// another part). -// BuildHoursSetting is the build modules' setting of the nodes' build windows: `=HH:MM-HH:MM` pairs separated by -// commas, in the node's local time; an end before its start crosses midnight (g14=18:00-06:00). +// BuildHoursSetting is the build modules' setting of the node's build window: HH:MM-HH:MM in the node's local time; an +// end before its start crosses midnight (18:00-06:00). const BuildHoursSetting = "build-hours" // BuildWindow is one node's build hours: minutes after local midnight, Start inclusive, End exclusive. @@ -51,35 +52,28 @@ func (w BuildWindow) NextEdge(t time.Time) time.Time { return next } -// ParseBuildHours is node's window in a build-hours setting: nil when the setting is empty or does not name the node. -func ParseBuildHours(setting, node string) (*BuildWindow, error) { - var found *BuildWindow - for _, pair := range strings.Split(setting, ",") { - pair = strings.TrimSpace(pair) - if pair == "" { - continue - } - name, span, ok := strings.Cut(pair, "=") - from, to, ok2 := strings.Cut(span, "-") - if !ok || !ok2 || strings.TrimSpace(name) == "" { - return nil, fmt.Errorf("the build hours %q: %q is not =HH:MM-HH:MM", setting, pair) - } - start, err := minuteOf(from) - if err != nil { - return nil, fmt.Errorf("the build hours %q: %v", setting, err) - } - end, err := minuteOf(to) - if err != nil { - return nil, fmt.Errorf("the build hours %q: %v", setting, err) - } - if start == end { - return nil, fmt.Errorf("the build hours %q: %s opens and closes at the same minute", setting, pair) - } - if strings.TrimSpace(name) == node { - found = &BuildWindow{Start: start, End: end} - } +// ParseBuildWindow reads a build-hours value, HH:MM-HH:MM: nil when it is empty. +func ParseBuildWindow(setting string) (*BuildWindow, error) { + setting = strings.TrimSpace(setting) + if setting == "" { + return nil, nil } - return found, nil + from, to, ok := strings.Cut(setting, "-") + if !ok { + return nil, fmt.Errorf("the build hours %q are not HH:MM-HH:MM", setting) + } + start, err := minuteOf(from) + if err != nil { + return nil, fmt.Errorf("the build hours %q: %v", setting, err) + } + end, err := minuteOf(to) + if err != nil { + return nil, fmt.Errorf("the build hours %q: %v", setting, err) + } + if start == end { + return nil, fmt.Errorf("the build hours %q open and close at the same minute", setting) + } + return &BuildWindow{Start: start, End: end}, nil } // minuteOf reads HH:MM as minutes after midnight. @@ -92,7 +86,7 @@ func minuteOf(s string) (int, error) { return h*60 + m, nil } -// NodeBuildHours is node's build window, read from the module's settings file at path at every call, and why it is +// NodeBuildHours is the node's build window, read from the module's settings file at path at every call, and why it is // none when the value cannot be read. No path, no file value or an empty one is no window and no fault. A value that // cannot be read is no window too, so the node builds at any hour; the setting is a preference of hours, not a safety // bound (a node that must never build has capacity 0). It is loud: the fault is said once per distinct fault through @@ -125,7 +119,7 @@ func NodeBuildHours(path, node string, logf func(string, ...any)) func() (*Build if v, ok := s[BuildHoursSetting]; ok && json.Unmarshal(v, &setting) != nil { return fault(fmt.Sprintf("the setting %s %s is not a string", BuildHoursSetting, v)) } - w, err := ParseBuildHours(setting, node) + w, err := ParseBuildWindow(setting) if err != nil { return fault(err.Error()) } diff --git a/go/buildstep/capacity.go b/go/buildstep/capacity.go index a6f128c..4e041a4 100644 --- a/go/buildstep/capacity.go +++ b/go/buildstep/capacity.go @@ -4,45 +4,38 @@ import ( "encoding/json" "fmt" "os" - - "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" ) -// CapacitySetting is the build modules' setting of each node's build capacity (novox/hq ADR 0525 §5): `=` pairs separated by commas, as the builder's was (ADR 0485 §1). Each build module declares it with -// DefaultCapacities as its default, and the controller lays it into the module's settings file. +// CapacitySetting is the build modules' setting of the node's build capacity (novox/hq ADR 0525 §5, ADR 0567 §12): how +// many asks the node takes at once, an integer config value set on each node's layer of mesh-config. No repository names +// a node: each build module declares it with the default 0, which pulls nothing, and the controller (or mesh-config) +// lays the node's value into the module's settings file. const CapacitySetting = "capacity" -// DefaultCapacities is the setting's default, the builder's before R4g: the control-node (novox) and the laptop (g14) -// build nothing. -const DefaultCapacities = "shanks=3,ace=2,g14=0,novox=0" - -// NodeCapacity is node's build capacity, read from the module's settings file at path at every call, so a change takes -// effect at the next pull; with no path it reads fallback. A node the setting does not name runs one ask at a time (as -// the builder's did). A file or a setting that cannot be read gives 0 and says why: pulling on a setting nobody can read -// could build on a node that must not. -func NodeCapacity(path, fallback, node string, logf func(string, ...any)) func() int { +// NodeCapacity is the node's build capacity, read from the module's settings file at path at every call, so a change +// takes effect at the next pull. No path, no file value, or a value that is not a whole number of at least 0 gives 0 and +// a value that cannot be read says why: pulling on a setting nobody can read could build on a node that must not. +func NodeCapacity(path, node string, logf func(string, ...any)) func() int { say := func(format string, args ...any) { if logf != nil { logf(format, args...) } } return func() int { - setting := fallback - if path != "" { - raw, err := os.ReadFile(path) - if err != nil { - say("the build capacity cannot be read, so %s pulls nothing: %v", node, err) - return 0 - } - var s map[string]any - if err := json.Unmarshal(raw, &s); err != nil { - say("the settings file %s is not JSON, so %s pulls nothing: %v", path, node, err) - return 0 - } - setting, _ = s[CapacitySetting].(string) + if path == "" { + return 0 } - n, err := capacityOf(setting, node) + raw, err := os.ReadFile(path) + if err != nil { + say("the build capacity cannot be read, so %s pulls nothing: %v", node, err) + return 0 + } + var s map[string]json.RawMessage + if err := json.Unmarshal(raw, &s); err != nil { + say("the settings file %s is not JSON, so %s pulls nothing: %v", path, node, err) + return 0 + } + n, err := capacityOf(s[CapacitySetting]) if err != nil { say("%v, so %s pulls nothing", err, node) return 0 @@ -51,11 +44,15 @@ func NodeCapacity(path, fallback, node string, logf func(string, ...any)) func() } } -// capacityOf is node's capacity in a setting's `=` pairs: DefaultCapacity for a node it does not name. -func capacityOf(setting, node string) (int, error) { - caps, err := buildrun.ParseCapacity(setting) - if err != nil { - return 0, fmt.Errorf("the build capacity: %w", err) +// capacityOf is the capacity a config value holds: absent or null is 0; anything but a whole number of at least 0 is +// refused, naming it. +func capacityOf(value json.RawMessage) (int, error) { + if len(value) == 0 || string(value) == "null" { + return 0, nil } - return (&buildrun.Places{Capacity: caps}).CapacityOf(node), nil + var n int + if err := json.Unmarshal(value, &n); err != nil || n < 0 { + return 0, fmt.Errorf("the build capacity %s is not a whole number of asks at once, 0 or more", value) + } + return n, nil } diff --git a/go/buildstep/capacity_cpu.go b/go/buildstep/capacity_cpu.go index 05123cc..413bfb9 100644 --- a/go/buildstep/capacity_cpu.go +++ b/go/buildstep/capacity_cpu.go @@ -14,7 +14,7 @@ import ( ) // **A build seat's capacity follows the CPU its node gives builds** (issue 1806's follow-up; issue 1890, 2026-10-26 -// 19:41: under the 50 % cap shanks' mesh-build-go kept capacity 3, three full mesh-catalog checks shared 12 cores, and +// 19:41: under the 50 % cap the workstation's mesh-build-go kept capacity 3, three full mesh-catalog checks shared 12 cores, and // each step ran past the 30-minute step bound with no test run). The configured capacity is an upper bound; the node's // build slice bounds it again by the cores a step needs: floor(the slice's quota in cores / cores-per-step), one at // least. The quota is read live from the cgroup at every judgement, so a changed cap is followed at the next take @@ -27,7 +27,7 @@ import ( const CoresPerStepSetting = "cores-per-step" // DefaultCoresPerStep are the build modules' defaults by language, from measured steps (2026-10-26): a full mesh-catalog -// check on mesh-build-go takes what it is given (three at once took 965 % of shanks' 1200 % slice, about 4 cores each, +// check on mesh-build-go takes what it is given (three at once took 965 % of the workstation's 1200 % slice, about 4 cores each, // and ran past the 30-minute step bound), so a Go step is given 6, half again what starved; a Node step (tsc, esbuild, a // test runner: median 15 s) and an image build (buildah, its RUN lines mostly fetches) need 2. var DefaultCoresPerStep = map[string]float64{delivery.AskGo: 6, delivery.AskNode: 2, delivery.AskImage: 2} diff --git a/go/buildstep/capacity_test.go b/go/buildstep/capacity_test.go index 6a22aa8..aef58c6 100644 --- a/go/buildstep/capacity_test.go +++ b/go/buildstep/capacity_test.go @@ -14,30 +14,20 @@ import ( // issue 1806's follow-up: the capacity judged by is the configured one bounded by the build slice's cores over the cores // a step needs (the three build nodes as they stand on 2026-10-26), and cores-per-step is read from the settings file. func TestNodeCapacityAndLimits(t *testing.T) { - if got := NodeCapacity("", DefaultCapacities, "novox", nil)(); got != 0 { - t.Errorf("novox = %d by default, want 0", got) - } - if got := NodeCapacity("", DefaultCapacities, "shanks", nil)(); got != 3 { - t.Errorf("shanks = %d by default, want 3", got) + // ADR 0567 §12: the capacity is the node's integer, its default 0, and no default names a node. + if got := NodeCapacity("", "laptop", nil)(); got != 0 { + t.Errorf("no settings file = %d, want 0", got) } file := filepath.Join(t.TempDir(), "settings.json") - os.WriteFile(file, []byte(`{"capacity":"shanks=1,ace=2"}`), 0o600) - capacity := NodeCapacity(file, DefaultCapacities, "shanks", nil) - if got := capacity(); got != 1 { - t.Errorf("shanks = %d from the file, want 1", got) + capacity := NodeCapacity(file, "anchor", nil) + for value, want := range map[string]int{`{"capacity":1}`: 1, `{"capacity":4}`: 4, `{}`: 0, `{"capacity":null}`: 0, + `{"capacity":"anchor=3"}`: 0, `{"capacity":-1}`: 0, `{"capacity":1.5}`: 0} { + os.WriteFile(file, []byte(value), 0o600) + if got := capacity(); got != want { + t.Errorf("%s = %d, want %d", value, got, want) + } } - os.WriteFile(file, []byte(`{"capacity":"shanks=4"}`), 0o600) - if got := capacity(); got != 4 { - t.Errorf("shanks = %d after the change, want 4", got) - } - if got := NodeCapacity(file, "", "pi", nil)(); got != 1 { - t.Errorf("a node not named = %d, want 1", got) - } - os.WriteFile(file, []byte(`{"capacity":"shanks=lots"}`), 0o600) - if got := capacity(); got != 0 { - t.Errorf("an unreadable setting = %d, want 0", got) - } - if got := NodeCapacity(filepath.Join(t.TempDir(), "none"), DefaultCapacities, "shanks", nil)(); got != 0 { + if got := NodeCapacity(filepath.Join(t.TempDir(), "none"), "anchor", nil)(); got != 0 { t.Errorf("a missing file = %d, want 0", got) } t.Setenv("MESH_BUILD_CPU_SHARE", "") @@ -74,7 +64,7 @@ func TestNodeCapacityAndLimits(t *testing.T) { for _, c := range []struct { file string want float64 - }{{`{"capacity":"shanks=2"}`, 6}, {`{"cores-per-step":4}`, 4}, {`{"cores-per-step":"3.5"}`, 3.5}, {`{"cores-per-step":0}`, 6}, {`{"cores-per-step":0}`, 6}} { + }{{`{"capacity":2}`, 6}, {`{"cores-per-step":4}`, 4}, {`{"cores-per-step":"3.5"}`, 3.5}, {`{"cores-per-step":0}`, 6}, {`{"cores-per-step":0}`, 6}} { os.WriteFile(file, []byte(c.file), 0o600) if got := cores(); got != c.want { t.Errorf("cores-per-step from %s = %g, want %g", c.file, got, c.want) @@ -100,18 +90,19 @@ func TestNodeCapacityAndLimits(t *testing.T) { 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, ""}, - {"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"}, + {"the laptop in the office", "18:00-06:00", "laptop", day(10, 0), 0, "18:00", false, ""}, + {"the laptop in the evening", "18:00-06:00", "laptop", day(18, 0), 2, "06:00", false, ""}, + {"the laptop past midnight", "18:00-06:00", "laptop", day(5, 59), 2, "06:00", false, ""}, + {"the laptop at the close", "18:00-06:00", "laptop", day(6, 0), 0, "18:00", false, ""}, + {"a day window", "09:00-17:30", "anchor", day(17, 29), 2, "17:30", false, ""}, + {"no window", "", "anchor", day(10, 0), 2, "", false, ""}, + {"a bad time", "18:00-6", "laptop", day(10, 0), 2, "", true, ""}, + {"no span", "18:00-18:00", "laptop", day(10, 0), 2, "", true, ""}, + {"the old form naming a node", "laptop=18:00-06:00", "laptop", day(10, 0), 2, "", true, ""}, + {"a node without /dev/net/tun", "", "anchor", day(10, 0), 0, "", false, "/dev/net/tun cannot be opened: no such device"}, + {"not eligible within its hours", "18:00-06:00", "laptop", day(19, 0), 0, "06:00", false, "/dev/net/tun cannot be opened: no such device"}, } { - w, err := ParseBuildHours(c.setting, c.node) + w, err := ParseBuildWindow(c.setting) if (err != nil) != c.bad { t.Errorf("%s: parse %v", c.name, err) } diff --git a/go/buildstep/phase.go b/go/buildstep/phase.go index 5ca842c..130178a 100644 --- a/go/buildstep/phase.go +++ b/go/buildstep/phase.go @@ -9,7 +9,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// **A running ask says its phases** (issue 1890, 2026-10-26 19:09: a mesh-catalog try's step on shanks ran 30 minutes +// **A running ask says its phases** (issue 1890, 2026-10-26 19:09: a mesh-catalog try's step on the workstation ran 30 minutes // and its module's journal held no line naming its ask, so nobody could tell whether it fetched, linted or tested). The // puller hands every ask's run a phase reporter in its context; RunAsk and a module's own step bodies call Phase as they // move (fetch and merge, a level's digest, lint, a level's tests), and each phase is logged with the ask's id and its diff --git a/go/buildstep/pull.go b/go/buildstep/pull.go index afa68e9..80c0a02 100644 --- a/go/buildstep/pull.go +++ b/go/buildstep/pull.go @@ -610,7 +610,7 @@ func (p *Puller) run(ctx context.Context, msg *Pulled, ask delivery.StepAsk, key } } -// watchCapacity follows the module's config value `capacity` when WatchConfig is set (issue 1887, 2026-10-25: g14's +// watchCapacity follows the module's config value `capacity` when WatchConfig is set (issue 1887, 2026-10-25: the laptop's // puller started at capacity 0 sat parked with capacity 2 set, because nothing woke it to read its settings file // again). The value comes as mesh-config resolved it for this node, the event of the change itself: the runtime hands // the value now and then each change its machine's provisioner client says moved, so nothing polls, and the puller does @@ -659,7 +659,7 @@ func (p *Puller) watchHours() { why = fmt.Sprintf("the setting %s %s is not a string", BuildHoursSetting, c.Value) default: var err error - if w, err = ParseBuildHours(setting, p.Node); err != nil { + if w, err = ParseBuildWindow(setting); err != nil { why = err.Error() } } @@ -689,13 +689,7 @@ func (p *Puller) capacityFrom(c stdio.ProvisionChange) (int, string) { if c.Refused != "" { return 0, "its capacity is no longer answered: " + c.Refused } - setting := "" - if len(c.Value) > 0 && string(c.Value) != "null" { - if err := json.Unmarshal(c.Value, &setting); err != nil { - return 0, fmt.Sprintf("its capacity %s is not a string", c.Value) - } - } - n, err := capacityOf(setting, p.Node) + n, err := capacityOf(c.Value) if err != nil { return 0, err.Error() } diff --git a/go/buildstep/pull_capacity_test.go b/go/buildstep/pull_capacity_test.go index 078409c..155b5de 100644 --- a/go/buildstep/pull_capacity_test.go +++ b/go/buildstep/pull_capacity_test.go @@ -22,7 +22,7 @@ func (w *configWatch) watch(key string, handle func(stdio.ProvisionChange) error w.asked, w.handle = key, handle value := w.value w.mu.Unlock() - return handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`"` + value + `"`)}) + return handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(value)}) } func (w *configWatch) set(t *testing.T, value string) { @@ -33,7 +33,7 @@ func (w *configWatch) set(t *testing.T, value string) { if handle == nil { t.Fatal("the puller watches no config value") } - if err := handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(`"` + value + `"`)}); err != nil { + if err := handle(stdio.ProvisionChange{Kind: "config", Key: key, Value: json.RawMessage(value)}); err != nil { t.Fatal(err) } } diff --git a/go/buildstep/pull_test.go b/go/buildstep/pull_test.go index 56a3ce1..165e673 100644 --- a/go/buildstep/pull_test.go +++ b/go/buildstep/pull_test.go @@ -282,7 +282,7 @@ func TestPullAtCapacityZeroTakesNothing(t *testing.T) { t.Run("config watch wakes", func(t *testing.T) { src, st := NewFakeAsks(), newSteps() _ = src.PublishAsk(testAsk(delivery.AskDelivery)) - w := &configWatch{value: "shanks=3,g14=0"} + w := &configWatch{value: "0"} p := &Puller{Source: src, Language: delivery.AskGo, Node: "g14", Put: st.put, PullWait: 10 * time.Millisecond, Capacity: func() int { return 0 }, // the settings file, not refilled yet WatchConfig: w.watch, @@ -298,7 +298,7 @@ func TestPullAtCapacityZeroTakesNothing(t *testing.T) { if w.asked != CapacitySetting { t.Fatalf("the puller watches %q, want %q", w.asked, CapacitySetting) } - w.set(t, "shanks=3,g14=2") + w.set(t, "2") waitFor(t, "the ask taken once the capacity moved to 2", func() bool { a, _, _ := src.Fates(); return len(a) == 1 }) }) for _, c := range []struct { diff --git a/go/buildstep/stale.go b/go/buildstep/stale.go index 988c04e..0172ff5 100644 --- a/go/buildstep/stale.go +++ b/go/buildstep/stale.go @@ -149,7 +149,7 @@ func endRunning(scan Scan, key string, step delivery.Step, revision uint64, why // asker takes the first end it reads, so an end written here would fail a check the redelivery would still run. Its // asker's watch takes puts only and keeps waiting for the redelivery's key, its bound the backstop; the stale running // key leaves the steps state, and with it the delivery view's and the checker's count of what runs (note 21: it -// held one of shanks' two slots). +// held one of a build node's two slots). // - A step owned by this process is left: it may run now. A redelivered ask begun on this node is owned by it. // - A step with no owner (written by an older build) keeps the earlier rule: a hand-started step started before // `before` (the start of this process) is ended; a pulled ask is left to the stream's redelivery (isAsk) until its diff --git a/go/delivery/buildrun/places.go b/go/delivery/buildrun/places.go index 4b189ba..d96dda6 100644 --- a/go/delivery/buildrun/places.go +++ b/go/delivery/buildrun/places.go @@ -4,38 +4,12 @@ import ( "context" "fmt" "sort" - "strconv" - "strings" "sync" ) // DefaultCapacity is the build capacity of a node that has no setting: one build step at a time. const DefaultCapacity = 1 -// ParseCapacity reads a capacity setting: `node=steps` pairs separated by commas, such as -// `shanks=3,ace=2`. A node without a pair has DefaultCapacity. A pair with no node, a count that is not a whole number of -// at least 0, and a node named twice are refused. A count of 0 keeps the part off the node: it is never chosen. -func ParseCapacity(s string) (map[string]int, error) { - out := map[string]int{} - for _, pair := range strings.Split(s, ",") { - pair = strings.TrimSpace(pair) - if pair == "" { - continue - } - node, count, ok := strings.Cut(pair, "=") - node, count = strings.TrimSpace(node), strings.TrimSpace(count) - n, err := strconv.Atoi(count) - if !ok || node == "" || err != nil || n < 0 { - return nil, fmt.Errorf("the capacity %q is not =", pair) - } - if _, dup := out[node]; dup { - return nil, fmt.Errorf("the capacity names %s twice", node) - } - out[node] = n - } - return out, nil -} - // Places counts the build steps a part runs on each node and holds a lane back while its node is at its // capacity (the operator's decision of 2026-10-21; ADR 0478 clause 3). A lane is the fetch and the builds of one build // module on one node, run one step at a time, so a lane holds one place while it runs. A node is chosen for a lane by diff --git a/go/delivery/buildrun/places_test.go b/go/delivery/buildrun/places_test.go index f7ff940..04386e7 100644 --- a/go/delivery/buildrun/places_test.go +++ b/go/delivery/buildrun/places_test.go @@ -7,21 +7,6 @@ import ( "time" ) -func TestParseCapacityReadsNodeEqualsCountPairs(t *testing.T) { - c, err := ParseCapacity(" shanks=3, ace=2 ") - if err != nil || c["shanks"] != 3 || c["ace"] != 2 || len(c) != 2 { - t.Errorf("capacity = %v, %v", c, err) - } - if c, err := ParseCapacity(""); err != nil || len(c) != 0 { - t.Errorf("empty: %v, %v", c, err) - } - for _, bad := range []string{"shanks", "shanks=x", "=2", "a=1,a=2", "shanks=-1"} { - if _, err := ParseCapacity(bad); err == nil { - t.Errorf("%q accepted", bad) - } - } -} - func acquireAsync(p *Places, node string) chan func() { got := make(chan func(), 1) go func() { @@ -95,17 +80,10 @@ func TestAWaitingStepStopsWhenItsContextEnds(t *testing.T) { // round 3 // The operator's decision of 2026-10-21 13:40: the builder builds on shanks and ace only; a capacity of 0 keeps it off a node. func TestACapacityOfZeroKeepsThePartOffTheNodeAndANodeNotNamedKeepsOne(t *testing.T) { // issue 1661 - c, err := ParseCapacity("shanks=3,ace=2,g14=0,novox=0") - if err != nil || c["g14"] != 0 || c["novox"] != 0 || c["shanks"] != 3 || len(c) != 4 { - t.Fatalf("capacity = %v, %v", c, err) - } - p := &Places{Capacity: c} + p := &Places{Capacity: map[string]int{"shanks": 3, "ace": 2, "g14": 0, "novox": 0}} if p.CapacityOf("g14") != 0 || p.CapacityOf("novox") != 0 || p.CapacityOf("ace") != 2 || p.CapacityOf("other") != DefaultCapacity { t.Errorf("capacities = %d %d %d %d, want 0 0 2 and the default for a node not named", p.CapacityOf("g14"), p.CapacityOf("novox"), p.CapacityOf("ace"), p.CapacityOf("other")) } - if _, err := ParseCapacity("shanks=-1"); err == nil { - t.Error("a negative capacity accepted") - } entries := tcs("go", "go1.27.1", "ace", "g14", "novox", "shanks", "other") for i := 0; i < 6; i++ { nodes, _, err := ChooseNodes("go", entries, p, nil, 2) @@ -119,7 +97,7 @@ func TestACapacityOfZeroKeepsThePartOffTheNodeAndANodeNotNamedKeepsOne(t *testin } } // Only nodes of capacity 0 report: nothing qualifies, and the build says why. - _, _, err = ChooseNodes("go", tcs("go", "go1.27.1", "g14", "novox"), p, nil, 2) + _, _, err := ChooseNodes("go", tcs("go", "go1.27.1", "g14", "novox"), p, nil, 2) if err == nil || !strings.Contains(err.Error(), "capacity") { t.Errorf("only capacity-0 nodes: error = %v, want it to name the capacity", err) }