From 8cd22892a3db214969e60aac1b6780f7d14a4551 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:22:57 +0200 Subject: [PATCH] =?UTF-8?q?Read=20a=20node's=20build=20capacity=20from=20t?= =?UTF-8?q?he=20build=20module's=20own=20setting=20at=20every=20pull,=20de?= =?UTF-8?q?faulting=20to=20the=20builder's=20values,=20so=20the=20control-?= =?UTF-8?q?node=20pulls=20nothing=20(issue=201887=20A7,=20ADR=200525=20?= =?UTF-8?q?=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/buildstep/askrun_test.go | 4 ++- go/buildstep/capacity.go | 52 +++++++++++++++++++++++++++++++++++ go/buildstep/capacity_test.go | 38 +++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 go/buildstep/capacity.go create mode 100644 go/buildstep/capacity_test.go diff --git a/go/buildstep/askrun_test.go b/go/buildstep/askrun_test.go index d023867..044d59d 100644 --- a/go/buildstep/askrun_test.go +++ b/go/buildstep/askrun_test.go @@ -92,7 +92,9 @@ build {"artifact":"lib","module":"modules/m","publish":true,"workspace":"/w/1"}` func TestRunAskRefusesAnotherToolchain(t *testing.T) { r := &recorder{fetch: map[string]any{"workspace": "/w"}} s := r.steps() - s.Toolchain = func(context.Context) (delivery.ToolchainUsed, error) { return delivery.ToolchainUsed{Version: "go1.27", Recipe: "3"}, nil } + s.Toolchain = func(context.Context) (delivery.ToolchainUsed, error) { + return delivery.ToolchainUsed{Version: "go1.27", Recipe: "3"}, nil + } _, err := RunAsk(context.Background(), s, ask(delivery.BuildJob{Module: "m", Path: ".", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}, Artifacts: []delivery.AskArtifact{{Artifact: "bundle"}}, Toolchain: &delivery.ToolchainUsed{Version: "go1.26", Recipe: "3"}})) var ref *Refusal diff --git a/go/buildstep/capacity.go b/go/buildstep/capacity.go new file mode 100644 index 0000000..607354f --- /dev/null +++ b/go/buildstep/capacity.go @@ -0,0 +1,52 @@ +package buildstep + +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. +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 { + 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) + } + caps, err := buildrun.ParseCapacity(setting) + if err != nil { + say("%v, so %s pulls nothing", fmt.Errorf("the build capacity: %w", err), node) + return 0 + } + return (&buildrun.Places{Capacity: caps}).CapacityOf(node) + } +} diff --git a/go/buildstep/capacity_test.go b/go/buildstep/capacity_test.go new file mode 100644 index 0000000..5334226 --- /dev/null +++ b/go/buildstep/capacity_test.go @@ -0,0 +1,38 @@ +package buildstep + +import ( + "os" + "path/filepath" + "testing" +) + +// 1887/A7: the node's build capacity is the build module's setting, read at every pull: the control-node at 0 pulls +// nothing, a change takes effect at the next read, and a setting that cannot be read pulls nothing. +func TestNodeCapacity(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) + } + 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) + } + 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 { + t.Errorf("a missing file = %d, want 0", got) + } +}