From 64b9ded2db4a3671fc72b2f3cf7dae29d7644dd7 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 13:41:05 +0100 Subject: [PATCH 1/4] Build modules: a per-node processor cap, and each step's threads within it (issue 1806) The operator's desktop slows while it builds (shanks at 92%, the Go build runtime at 1650% of 24 processors). The build modules declare cpu-cap (default shanks=50,g14=50,ace=75), which the composer turns into the runtime's quota at idle priority; each step's go, node and buildah commands get GOMAXPROCS and UV_THREADPOOL_SIZE for their part of it, and the status tools show the limits and what the kernel holds the runtime to. --- .../mesh-build-go/cmd/mesh-build-go/build.go | 4 +- .../mesh-build-go/cmd/mesh-build-go/cpu.go | 39 +++++++++++++++++++ .../mesh-build-go/cmd/mesh-build-go/fetch.go | 4 +- .../mesh-build-go/installed_toolchain_test.go | 19 +++++++++ .../cmd/mesh-build-go/isolate.go | 2 +- .../mesh-build-go/cmd/mesh-build-go/module.go | 3 +- modules/mesh-build-go/files/settings.json | 3 +- modules/mesh-build-go/go.mod | 2 +- modules/mesh-build-go/go.sum | 4 +- modules/mesh-build-go/module.json | 8 +++- .../cmd/mesh-build-image/build_test.go | 3 ++ .../cmd/mesh-build-image/cpu.go | 39 +++++++++++++++++++ .../cmd/mesh-build-image/module.go | 3 +- .../cmd/mesh-build-image/probe.go | 2 +- modules/mesh-build-image/files/settings.json | 3 +- modules/mesh-build-image/go.mod | 2 +- modules/mesh-build-image/go.sum | 4 +- modules/mesh-build-image/module.json | 8 +++- .../cmd/mesh-build-node/cpu.go | 39 +++++++++++++++++++ .../cmd/mesh-build-node/fetch_test.go | 6 +++ .../cmd/mesh-build-node/isolate.go | 2 +- .../cmd/mesh-build-node/module.go | 5 ++- modules/mesh-build-node/files/settings.json | 3 +- modules/mesh-build-node/go.mod | 2 +- modules/mesh-build-node/go.sum | 4 +- modules/mesh-build-node/module.json | 8 +++- 26 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/cpu.go create mode 100644 modules/mesh-build-image/cmd/mesh-build-image/cpu.go create mode 100644 modules/mesh-build-node/cmd/mesh-build-node/cpu.go diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build.go b/modules/mesh-build-go/cmd/mesh-build-go/build.go index b1f89d7e..40eec5bf 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build.go @@ -183,12 +183,12 @@ func (m *Module) buildEnv(dir string) []string { } else if modfile, ok := m.pinModfile(dir); ok { mod += " -modfile=" + modfile // a pin on a pull request's head the fetch placed beside the workspace (pins.go) } - return []string{ + return append([]string{ "CGO_ENABLED=0", "GOFLAGS=" + recipeFlags + " " + mod, "GOTOOLCHAIN=local", "GOMODCACHE=" + filepath.Join(m.Cache, "gomod"), "GOCACHE=" + filepath.Join(m.Cache, "gobuild"), "GOPROXY=off", - } + }, m.threadEnv()...) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/cpu.go b/modules/mesh-build-go/cmd/mesh-build-go/cpu.go new file mode 100644 index 00000000..73322827 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/cpu.go @@ -0,0 +1,39 @@ +package main + +import ( + "git.novox.be/novox/mesh-sdk/go/buildcpu" + "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to +// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that +// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's +// throttle: GOMAXPROCS, which sets every go command's -p, the compiler's concurrency, a test binary's processors and +// -parallel. + +// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's +// capacity, read at every call like the capacity, so a change takes effect at the next step. +func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { + slots := 1 + switch { + case m.Capacity != nil: + slots = m.Capacity() + case m.Settings != "": + slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)() + } + if slots < 1 { + slots = 1 + } + return buildstep.NodeLimits(m.Settings, m.Node, slots, say) +} + +// threadEnv is the environment every go command of a step is given for its threads. +func (m *Module) threadEnv() []string { + return buildcpu.Env(m.cpuLimits(nil).Threads) +} + +// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime +// to, read from its cgroup rather than assumed. +func (m *Module) cpuStatus() map[string]any { + return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()} +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go index 6dc0e424..710d5cee 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -292,7 +292,7 @@ func (m *Module) goEnv() []string { if m.GoModCache != "" { modcache = m.GoModCache } - return []string{ + return append([]string{ "GOTOOLCHAIN=local", "GOFLAGS=-mod=mod", "GOMODCACHE=" + modcache, @@ -300,7 +300,7 @@ func (m *Module) goEnv() []string { "GOPRIVATE=git.novox.be", "GIT_TERMINAL_PROMPT=0", "GIT_SSH_COMMAND=ssh -o BatchMode=yes", - } + }, m.threadEnv()...) } // goEnvFor is goEnv for a command run in dir: a directory that vendors (vendor/modules.txt) is read from its vendor diff --git a/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go b/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go index 22467181..6ca48aa8 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go @@ -5,10 +5,13 @@ import ( "os" "path/filepath" "reflect" + "runtime" + "strconv" "strings" "testing" "time" + "git.novox.be/novox/mesh-sdk/go/buildcpu" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/forgekey" "git.novox.be/novox/mesh-sdk/go/health" @@ -41,6 +44,22 @@ func TestEveryStepEnvironmentSaysGotoolchainLocal(t *testing.T) { if got := toolchainValues(env); len(got) != 1 || got[0] != "GOTOOLCHAIN=local" { t.Errorf("%s: GOTOOLCHAIN settings %v, want exactly GOTOOLCHAIN=local", name, got) } + // Issue 1806: and its threads, its part of the node's processor cap (probeEnv runs no build). + if procs := strings.Count(strings.Join(env, "\n"), "GOMAXPROCS="); name != "probeEnv" && procs != 1 { + t.Errorf("%s: GOMAXPROCS said %d times, want once: %v", name, procs, env) + } + } + // Three steps at once under shanks' default half of its processors share them: each gets a third of the half. + settings := filepath.Join(t.TempDir(), "settings.json") + os.WriteFile(settings, []byte(`{"capacity":"shanks=3","cpu-cap":"shanks=50"}`), 0o600) + m = &Module{Cache: t.TempDir(), Node: "shanks", Settings: settings} + l := m.cpuLimits(nil) + if want := buildcpu.Threads(50, runtime.NumCPU(), 3); l.Percent != 50 || l.Slots != 3 || l.Threads != want || + !strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS="+strconv.Itoa(want)) { + t.Errorf("limits %+v, goEnv %v; want %d threads", l, m.goEnv(), want) + } + if st := m.cpuStatus(); st["limits"].(buildcpu.Limits) != l { + t.Errorf("the status says %v", st) } } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/isolate.go b/modules/mesh-build-go/cmd/mesh-build-go/isolate.go index a50bea28..b47538e9 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/isolate.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/isolate.go @@ -40,7 +40,7 @@ func (m *Module) isolation(workspace string, write bool) buildstep.Isolation { Workspace: filepath.Join(m.Work, workspace), Caches: m.caches(), WriteCaches: write, - Env: []string{"PATH=" + path, "LC_ALL=C"}, + Env: append([]string{"PATH=" + path, "LC_ALL=C"}, m.threadEnv()...), } } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/module.go b/modules/mesh-build-go/cmd/mesh-build-go/module.go index f7ef5317..6b6b4d5b 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/module.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/module.go @@ -184,13 +184,14 @@ func (m *Module) Short(run func(ctx context.Context, args map[string]any) (any, } // Status is what the module reports of itself: the node, the recipe, the toolchain found and why it refuses, if it -// does. +// does, and the processors its steps may take (issue 1806). func (m *Module) Status(ctx context.Context) map[string]any { err := m.Check(ctx) installed, _ := m.Probe(ctx) out := map[string]any{ "node": m.Node, "recipe": m.Recipe.Version(), "installed": installed, "postgres-major": m.Recipe.PostgresMajor, "nats-server": m.Recipe.NatsServer, "refusing": err != nil, + "cpu": m.cpuStatus(), } if err != nil { out["why"] = err.Error() diff --git a/modules/mesh-build-go/files/settings.json b/modules/mesh-build-go/files/settings.json index 3175fe39..31e080c2 100644 --- a/modules/mesh-build-go/files/settings.json +++ b/modules/mesh-build-go/files/settings.json @@ -1,3 +1,4 @@ { - "capacity": "shanks=3,ace=2,g14=0,novox=0" + "capacity": "shanks=3,ace=2,g14=0,novox=0", + "cpu-cap": "shanks=50,g14=50,ace=75" } diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index 728a1896..97d13d34 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index 36b59fa1..cee307a9 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4 h1:05sVel/UOvz4/g0dxrDOeVUL7BGakVrxPBlqRq02ZhM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e h1:9Kwe61NVrT4NcNr6PZoupzsGuZCQ5WV+luqrxDV9frs= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index 6346a6ad..7742aab8 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -29,6 +29,12 @@ "description": "How many asks of the Go build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.", "pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*", "default": "shanks=3,ace=2,g14=0,novox=0" + }, + "cpu-cap": { + "type": "string", + "description": "The most of each node's processors this module's builds may take, as node=percent pairs separated by commas, such as shanks=50,ace=75 (novox/hq issue 1806); a node not named takes 50. The node-engine holds the module's runtime to it as a CPU quota, at idle priority, and each build step starts no more threads than its part of it.", + "pattern": "|[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?(,[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?)*", + "default": "shanks=50,g14=50,ace=75" } }, "tools": [ @@ -106,7 +112,7 @@ "path": "${dir:state}/settings.json", "mode": "0600", "owner": "mesh-build-go", - "takes-config": ["capacity"], + "takes-config": ["capacity", "cpu-cap"], "merge": "json", "template": "files/settings.json" }, diff --git a/modules/mesh-build-image/cmd/mesh-build-image/build_test.go b/modules/mesh-build-image/cmd/mesh-build-image/build_test.go index 966c3adc..d146bfab 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/build_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/build_test.go @@ -110,6 +110,9 @@ func TestBuildRunsBuildahRootlessAndAnswersTheDigest(t *testing.T) { if !strings.Contains(env, "XDG_RUNTIME_DIR="+m.Store+"/xdg") { t.Errorf("the environment of %q lacks the module's own runtime directory: %s", c.line, env) } + if !strings.Contains(env, "GOMAXPROCS=") { + t.Errorf("the environment of %q gives buildah no thread limit (issue 1806): %s", c.line, env) + } } // The image is removed from the store once the digest is read, so the store does not grow with every build. if len(f.called("rmi")) != 1 { diff --git a/modules/mesh-build-image/cmd/mesh-build-image/cpu.go b/modules/mesh-build-image/cmd/mesh-build-image/cpu.go new file mode 100644 index 00000000..d38b9616 --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/cpu.go @@ -0,0 +1,39 @@ +package main + +import ( + "git.novox.be/novox/mesh-sdk/go/buildcpu" + "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to +// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that +// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's +// throttle. + +// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's +// capacity, read at every call like the capacity, so a change takes effect at the next step. +func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { + slots := 1 + switch { + case m.Capacity != nil: + slots = m.Capacity() + case m.Settings != "": + slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)() + } + if slots < 1 { + slots = 1 + } + return buildstep.NodeLimits(m.Settings, m.Node, slots, say) +} + +// threadEnv is the environment buildah is given for its threads (it is a Go program). What a build's RUN lines start +// runs in the container's own environment, inside the runtime's cgroup: the quota and the nice level bind it. +func (m *Module) threadEnv() []string { + return buildcpu.Env(m.cpuLimits(nil).Threads) +} + +// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime +// to, read from its cgroup rather than assumed. +func (m *Module) cpuStatus() map[string]any { + return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()} +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/module.go b/modules/mesh-build-image/cmd/mesh-build-image/module.go index 76a14c71..9747efaa 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/module.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/module.go @@ -128,7 +128,8 @@ func (m *Module) Health(ctx context.Context) health.Answer { // Status is what the module reports of itself. func (m *Module) Status(ctx context.Context) map[string]any { l, err := m.look(ctx) - out := map[string]any{"node": m.Node, "account": m.Account, "buildah": l.Buildah, "subuids": l.UIDs, "subgids": l.GIDs, "refusing": err != nil} + out := map[string]any{"node": m.Node, "account": m.Account, "buildah": l.Buildah, "subuids": l.UIDs, "subgids": l.GIDs, "refusing": err != nil, + "cpu": m.cpuStatus()} if err != nil { out["why"] = err.Error() } diff --git a/modules/mesh-build-image/cmd/mesh-build-image/probe.go b/modules/mesh-build-image/cmd/mesh-build-image/probe.go index e96c2212..ed3b96f5 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/probe.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/probe.go @@ -117,7 +117,7 @@ func (m *Module) storeFlags() []string { // error, and the next look refuses with it. func (m *Module) env() []string { _ = m.ensureRuntimeDir() - return []string{"XDG_RUNTIME_DIR=" + m.runtimeDir()} + return append([]string{"XDG_RUNTIME_DIR=" + m.runtimeDir()}, m.threadEnv()...) } // runtimeDir is buildah's runtime directory, inside the store. diff --git a/modules/mesh-build-image/files/settings.json b/modules/mesh-build-image/files/settings.json index 3175fe39..31e080c2 100644 --- a/modules/mesh-build-image/files/settings.json +++ b/modules/mesh-build-image/files/settings.json @@ -1,3 +1,4 @@ { - "capacity": "shanks=3,ace=2,g14=0,novox=0" + "capacity": "shanks=3,ace=2,g14=0,novox=0", + "cpu-cap": "shanks=50,g14=50,ace=75" } diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index d61c482e..592e12bf 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -3,7 +3,7 @@ module meshbuildimage go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-image/go.sum b/modules/mesh-build-image/go.sum index 8a877fdd..08be197a 100644 --- a/modules/mesh-build-image/go.sum +++ b/modules/mesh-build-image/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8 h1:x4mvY7yiGTP5lnzxbiwyj1o24p05hAmzZP7xHg3XX2M= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e h1:9Kwe61NVrT4NcNr6PZoupzsGuZCQ5WV+luqrxDV9frs= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index 6e25dbfa..41e08763 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -24,6 +24,12 @@ "description": "How many asks of the image build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.", "pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*", "default": "shanks=3,ace=2,g14=0,novox=0" + }, + "cpu-cap": { + "type": "string", + "description": "The most of each node's processors this module's builds may take, as node=percent pairs separated by commas, such as shanks=50,ace=75 (novox/hq issue 1806); a node not named takes 50. The node-engine holds the module's runtime to it as a CPU quota, at idle priority, and each build step starts no more threads than its part of it.", + "pattern": "|[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?(,[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?)*", + "default": "shanks=50,g14=50,ace=75" } }, "tools": [ @@ -77,7 +83,7 @@ "path": "${dir:state}/settings.json", "mode": "0600", "owner": "mesh-build-image", - "takes-config": ["capacity"], + "takes-config": ["capacity", "cpu-cap"], "merge": "json", "template": "files/settings.json" }, diff --git a/modules/mesh-build-node/cmd/mesh-build-node/cpu.go b/modules/mesh-build-node/cmd/mesh-build-node/cpu.go new file mode 100644 index 00000000..5221728a --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/cpu.go @@ -0,0 +1,39 @@ +package main + +import ( + "git.novox.be/novox/mesh-sdk/go/buildcpu" + "git.novox.be/novox/mesh-sdk/go/buildstep" +) + +// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to +// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that +// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's +// throttle. + +// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's +// capacity, read at every call like the capacity, so a change takes effect at the next step. +func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { + slots := 1 + switch { + case m.Capacity != nil: + slots = m.Capacity() + case m.Settings != "": + slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)() + } + if slots < 1 { + slots = 1 + } + return buildstep.NodeLimits(m.Settings, m.Node, slots, say) +} + +// threadEnv is the environment every command of a step is given for its threads: GOMAXPROCS for esbuild and every Go +// program, UV_THREADPOOL_SIZE for Node's worker pool. +func (m *Module) threadEnv() []string { + return buildcpu.Env(m.cpuLimits(nil).Threads) +} + +// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime +// to, read from its cgroup rather than assumed. +func (m *Module) cpuStatus() map[string]any { + return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()} +} diff --git a/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go b/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go index 07ebeb09..a976fb92 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "slices" "strings" "testing" "time" @@ -245,6 +246,11 @@ func TestNoCommandOfTheStepSeesATokenOfTheProcess(t *testing.T) { if len(c.env) < 3 || !strings.HasPrefix(c.env[0], "PATH=") || !strings.HasPrefix(c.env[1], "HOME=") || c.env[2] != "LC_ALL=C" { t.Errorf("%s was given %v, want PATH, HOME and the locale first", c.name, c.env) } + // And its threads, its part of the node's processor cap (issue 1806). + if !slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "GOMAXPROCS=") }) || + !slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "UV_THREADPOOL_SIZE=") }) { + t.Errorf("%s was given %v, without its threads", c.name, c.env) + } } // The runner main wires: what it hands a command is exactly what the step gave. ran := forgekey.ExecRunnerClean(10*time.Second)(context.Background(), append(m.baseEnv(), "ONLY=this"), "env") diff --git a/modules/mesh-build-node/cmd/mesh-build-node/isolate.go b/modules/mesh-build-node/cmd/mesh-build-node/isolate.go index b6222da9..fbc985e4 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/isolate.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/isolate.go @@ -35,7 +35,7 @@ func (m *Module) isolation(root string, write bool) buildstep.Isolation { ReadOnly: []string{m.toolchainDir()}, Caches: []string{m.npmCache()}, WriteCaches: write, - Env: []string{"PATH=" + pathOrDefault(), "LC_ALL=C"}, + Env: append([]string{"PATH=" + pathOrDefault(), "LC_ALL=C"}, m.threadEnv()...), } } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/module.go b/modules/mesh-build-node/cmd/mesh-build-node/module.go index 1517b159..ae0bde6f 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/module.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/module.go @@ -202,7 +202,8 @@ func (m *Module) Status(ctx context.Context) map[string]any { err := m.Check(ctx) installed, _ := m.Probe(ctx) r := m.recipe() - out := map[string]any{"node": m.Node, "recipe": r.Version(), "installed": installed, "refusing": err != nil} + out := map[string]any{"node": m.Node, "recipe": r.Version(), "installed": installed, "refusing": err != nil, + "cpu": m.cpuStatus()} if m.Toolchain != nil { out["toolchain-dir"] = m.Toolchain.Paths.Dir(r.Version()) out["toolchain-installed"] = m.Toolchain.Present(r.Version()) @@ -227,7 +228,7 @@ const defaultPath = "/usr/local/bin:/usr/bin:/bin" // files and git configuration) and a fixed locale. A command is given these and its own words, never this process's // environment, which holds the module's bus credentials (ADR 0466 §4). func (m *Module) baseEnv() []string { - return []string{"PATH=" + pathOrDefault(), "HOME=" + m.Toolchain.homeOrEmpty(), "LC_ALL=C"} + return append([]string{"PATH=" + pathOrDefault(), "HOME=" + m.Toolchain.homeOrEmpty(), "LC_ALL=C"}, m.threadEnv()...) } func pathOrDefault() string { diff --git a/modules/mesh-build-node/files/settings.json b/modules/mesh-build-node/files/settings.json index 3175fe39..31e080c2 100644 --- a/modules/mesh-build-node/files/settings.json +++ b/modules/mesh-build-node/files/settings.json @@ -1,3 +1,4 @@ { - "capacity": "shanks=3,ace=2,g14=0,novox=0" + "capacity": "shanks=3,ace=2,g14=0,novox=0", + "cpu-cap": "shanks=50,g14=50,ace=75" } diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index 443f8309..abd141fd 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -3,7 +3,7 @@ module meshbuildnode go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-node/go.sum b/modules/mesh-build-node/go.sum index e074515f..753e4d8c 100644 --- a/modules/mesh-build-node/go.sum +++ b/modules/mesh-build-node/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4 h1:05sVel/UOvz4/g0dxrDOeVUL7BGakVrxPBlqRq02ZhM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e h1:9Kwe61NVrT4NcNr6PZoupzsGuZCQ5WV+luqrxDV9frs= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index 993fb7a1..dace2a37 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -25,6 +25,12 @@ "description": "How many asks of the Node build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.", "pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*", "default": "shanks=3,ace=2,g14=0,novox=0" + }, + "cpu-cap": { + "type": "string", + "description": "The most of each node's processors this module's builds may take, as node=percent pairs separated by commas, such as shanks=50,ace=75 (novox/hq issue 1806); a node not named takes 50. The node-engine holds the module's runtime to it as a CPU quota, at idle priority, and each build step starts no more threads than its part of it.", + "pattern": "|[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?(,[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?)*", + "default": "shanks=50,g14=50,ace=75" } }, "tools": [ @@ -99,7 +105,7 @@ "path": "${dir:state}/settings.json", "mode": "0600", "owner": "mesh-build-node", - "takes-config": ["capacity"], + "takes-config": ["capacity", "cpu-cap"], "merge": "json", "template": "files/settings.json" }, -- 2.54.0 From 21583d14ad77396a2a73491d3dba03abbad4512a Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 13:49:54 +0100 Subject: [PATCH 2/4] Pin mesh-sdk at fix/build-cpu-yields' head (issue 1806) --- modules/mesh-build-go/go.mod | 2 +- modules/mesh-build-go/go.sum | 4 ++-- modules/mesh-build-image/go.mod | 2 +- modules/mesh-build-image/go.sum | 4 ++-- modules/mesh-build-node/go.mod | 2 +- modules/mesh-build-node/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index 97d13d34..ecb04bd9 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index cee307a9..07bac378 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e h1:9Kwe61NVrT4NcNr6PZoupzsGuZCQ5WV+luqrxDV9frs= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 h1:DugsX7h46z+1FOiNhtDE43Vf1WInz2vgwi0mn/1VZDg= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index 592e12bf..cda53ec0 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -3,7 +3,7 @@ module meshbuildimage go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-image/go.sum b/modules/mesh-build-image/go.sum index 08be197a..6ca74757 100644 --- a/modules/mesh-build-image/go.sum +++ b/modules/mesh-build-image/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e h1:9Kwe61NVrT4NcNr6PZoupzsGuZCQ5WV+luqrxDV9frs= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 h1:DugsX7h46z+1FOiNhtDE43Vf1WInz2vgwi0mn/1VZDg= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index abd141fd..a7882138 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -3,7 +3,7 @@ module meshbuildnode go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-node/go.sum b/modules/mesh-build-node/go.sum index 753e4d8c..3a53c941 100644 --- a/modules/mesh-build-node/go.sum +++ b/modules/mesh-build-node/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e h1:9Kwe61NVrT4NcNr6PZoupzsGuZCQ5WV+luqrxDV9frs= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026123700-04902ff24a0e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 h1:DugsX7h46z+1FOiNhtDE43Vf1WInz2vgwi0mn/1VZDg= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= -- 2.54.0 From dcdf4238c7d850de1db143f47fbd039ef3362cdd Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 14:22:05 +0100 Subject: [PATCH 3/4] Build modules: no cap of their own; a step's threads are its part of the node's one share (ADR 0553, issue 1806) Review round 1: a cap per module let three build modules take three shares of the node. The share is now one node value held by the node-engine's yielding slice; each step divides it by every build slot of the node, from the composer's words, or assumes every build seat at its own capacity without them. --- .../mesh-build-go/cmd/mesh-build-go/cpu.go | 30 +++++++++---------- .../mesh-build-go/installed_toolchain_test.go | 21 ++++++++----- modules/mesh-build-go/files/settings.json | 3 +- modules/mesh-build-go/go.mod | 2 +- modules/mesh-build-go/go.sum | 4 +-- modules/mesh-build-go/module.json | 8 +---- .../cmd/mesh-build-image/cpu.go | 28 ++++++++--------- modules/mesh-build-image/files/settings.json | 3 +- modules/mesh-build-image/go.mod | 2 +- modules/mesh-build-image/go.sum | 4 +-- modules/mesh-build-image/module.json | 8 +---- .../cmd/mesh-build-node/cpu.go | 26 ++++++++-------- modules/mesh-build-node/files/settings.json | 3 +- modules/mesh-build-node/go.mod | 2 +- modules/mesh-build-node/go.sum | 4 +-- modules/mesh-build-node/module.json | 8 +---- 16 files changed, 67 insertions(+), 89 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/cpu.go b/modules/mesh-build-go/cmd/mesh-build-go/cpu.go index 73322827..dc7c2a55 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/cpu.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/cpu.go @@ -5,15 +5,15 @@ import ( "git.novox.be/novox/mesh-sdk/go/buildstep" ) -// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to -// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that -// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's -// throttle: GOMAXPROCS, which sets every go command's -p, the compiler's concurrency, a test binary's processors and -// -parallel. +// The processors a step may take (novox/hq ADR 0553, issue 1806). The node-engine holds this module's runtime, with +// every other build module's of the node, in one slice at idle priority under the node's one share of its processors; +// inside that bound a step starts no more threads than its part of it: the share divided by every build slot of the +// node, from the words the composer gives (buildstep.NodeLimits), so the steps running at once do not wait in the +// kernel's throttle. -// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's -// capacity, read at every call like the capacity, so a change takes effect at the next step. -func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { +// cpuLimits is what one step is held to now. Without the composer's words, it assumes every build seat of the node +// beside it at this module's own capacity. +func (m *Module) cpuLimits() buildcpu.Limits { slots := 1 switch { case m.Capacity != nil: @@ -21,19 +21,17 @@ func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { case m.Settings != "": slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)() } - if slots < 1 { - slots = 1 - } - return buildstep.NodeLimits(m.Settings, m.Node, slots, say) + return buildstep.NodeLimits(slots) } -// threadEnv is the environment every go command of a step is given for its threads. +// threadEnv is the environment every go command of a step is given for its threads: GOMAXPROCS sets its -p, the compiler's +// concurrency, a test binary's processors and -parallel. func (m *Module) threadEnv() []string { - return buildcpu.Env(m.cpuLimits(nil).Threads) + return buildcpu.Env(m.cpuLimits().Threads) } // cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime -// to, read from its cgroup rather than assumed. +// to (its unit and its slice), read from its cgroup rather than assumed. func (m *Module) cpuStatus() map[string]any { - return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()} + return map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()} } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go b/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go index 6ca48aa8..a498c044 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go @@ -49,16 +49,21 @@ func TestEveryStepEnvironmentSaysGotoolchainLocal(t *testing.T) { t.Errorf("%s: GOMAXPROCS said %d times, want once: %v", name, procs, env) } } - // Three steps at once under shanks' default half of its processors share them: each gets a third of the half. - settings := filepath.Join(t.TempDir(), "settings.json") - os.WriteFile(settings, []byte(`{"capacity":"shanks=3","cpu-cap":"shanks=50"}`), 0o600) - m = &Module{Cache: t.TempDir(), Node: "shanks", Settings: settings} - l := m.cpuLimits(nil) - if want := buildcpu.Threads(50, runtime.NumCPU(), 3); l.Percent != 50 || l.Slots != 3 || l.Threads != want || - !strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS="+strconv.Itoa(want)) { + // Three slots of its own and no words from the composer: every build seat beside it at three, at the default + // share; with the composer's words, its part of the node's share (ADR 0553 §4). + t.Setenv("MESH_BUILD_CPU_SHARE", "") + t.Setenv("MESH_BUILD_SLOTS", "") + m = &Module{Cache: t.TempDir(), Node: "shanks", Capacity: func() int { return 3 }} + if l := m.cpuLimits(); l.Percent != 50 || l.Slots != 9 || l.Threads != buildcpu.Threads(50, runtime.NumCPU(), 9) { + t.Errorf("without words: %+v", l) + } + t.Setenv("MESH_BUILD_CPU_SHARE", "50") + t.Setenv("MESH_BUILD_SLOTS", "4") + want := buildcpu.Threads(50, runtime.NumCPU(), 4) + if l := m.cpuLimits(); l.Slots != 4 || !strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS="+strconv.Itoa(want)) { t.Errorf("limits %+v, goEnv %v; want %d threads", l, m.goEnv(), want) } - if st := m.cpuStatus(); st["limits"].(buildcpu.Limits) != l { + if st := m.cpuStatus(); st["limits"].(buildcpu.Limits) != m.cpuLimits() { t.Errorf("the status says %v", st) } } diff --git a/modules/mesh-build-go/files/settings.json b/modules/mesh-build-go/files/settings.json index 31e080c2..3175fe39 100644 --- a/modules/mesh-build-go/files/settings.json +++ b/modules/mesh-build-go/files/settings.json @@ -1,4 +1,3 @@ { - "capacity": "shanks=3,ace=2,g14=0,novox=0", - "cpu-cap": "shanks=50,g14=50,ace=75" + "capacity": "shanks=3,ace=2,g14=0,novox=0" } diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index ecb04bd9..54bdeaf8 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index 07bac378..3eb507ac 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 h1:DugsX7h46z+1FOiNhtDE43Vf1WInz2vgwi0mn/1VZDg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1 h1:6O9WKnlVR1uHV5DDQueHGbMAQzfVS06J7DBKjUPBG4E= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index 7742aab8..6346a6ad 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -29,12 +29,6 @@ "description": "How many asks of the Go build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.", "pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*", "default": "shanks=3,ace=2,g14=0,novox=0" - }, - "cpu-cap": { - "type": "string", - "description": "The most of each node's processors this module's builds may take, as node=percent pairs separated by commas, such as shanks=50,ace=75 (novox/hq issue 1806); a node not named takes 50. The node-engine holds the module's runtime to it as a CPU quota, at idle priority, and each build step starts no more threads than its part of it.", - "pattern": "|[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?(,[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?)*", - "default": "shanks=50,g14=50,ace=75" } }, "tools": [ @@ -112,7 +106,7 @@ "path": "${dir:state}/settings.json", "mode": "0600", "owner": "mesh-build-go", - "takes-config": ["capacity", "cpu-cap"], + "takes-config": ["capacity"], "merge": "json", "template": "files/settings.json" }, diff --git a/modules/mesh-build-image/cmd/mesh-build-image/cpu.go b/modules/mesh-build-image/cmd/mesh-build-image/cpu.go index d38b9616..1e253ff6 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/cpu.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/cpu.go @@ -5,14 +5,15 @@ import ( "git.novox.be/novox/mesh-sdk/go/buildstep" ) -// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to -// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that -// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's -// throttle. +// The processors a step may take (novox/hq ADR 0553, issue 1806). The node-engine holds this module's runtime, with +// every other build module's of the node, in one slice at idle priority under the node's one share of its processors; +// inside that bound a step starts no more threads than its part of it: the share divided by every build slot of the +// node, from the words the composer gives (buildstep.NodeLimits), so the steps running at once do not wait in the +// kernel's throttle. -// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's -// capacity, read at every call like the capacity, so a change takes effect at the next step. -func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { +// cpuLimits is what one step is held to now. Without the composer's words, it assumes every build seat of the node +// beside it at this module's own capacity. +func (m *Module) cpuLimits() buildcpu.Limits { slots := 1 switch { case m.Capacity != nil: @@ -20,20 +21,17 @@ func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { case m.Settings != "": slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)() } - if slots < 1 { - slots = 1 - } - return buildstep.NodeLimits(m.Settings, m.Node, slots, say) + return buildstep.NodeLimits(slots) } // threadEnv is the environment buildah is given for its threads (it is a Go program). What a build's RUN lines start -// runs in the container's own environment, inside the runtime's cgroup: the quota and the nice level bind it. +// runs in the container's own environment, inside the runtime's cgroup: the slice's quota and the nice level bind it. func (m *Module) threadEnv() []string { - return buildcpu.Env(m.cpuLimits(nil).Threads) + return buildcpu.Env(m.cpuLimits().Threads) } // cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime -// to, read from its cgroup rather than assumed. +// to (its unit and its slice), read from its cgroup rather than assumed. func (m *Module) cpuStatus() map[string]any { - return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()} + return map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()} } diff --git a/modules/mesh-build-image/files/settings.json b/modules/mesh-build-image/files/settings.json index 31e080c2..3175fe39 100644 --- a/modules/mesh-build-image/files/settings.json +++ b/modules/mesh-build-image/files/settings.json @@ -1,4 +1,3 @@ { - "capacity": "shanks=3,ace=2,g14=0,novox=0", - "cpu-cap": "shanks=50,g14=50,ace=75" + "capacity": "shanks=3,ace=2,g14=0,novox=0" } diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index cda53ec0..bd683c1c 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -3,7 +3,7 @@ module meshbuildimage go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-image/go.sum b/modules/mesh-build-image/go.sum index 6ca74757..1679d427 100644 --- a/modules/mesh-build-image/go.sum +++ b/modules/mesh-build-image/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 h1:DugsX7h46z+1FOiNhtDE43Vf1WInz2vgwi0mn/1VZDg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1 h1:6O9WKnlVR1uHV5DDQueHGbMAQzfVS06J7DBKjUPBG4E= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index 41e08763..6e25dbfa 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -24,12 +24,6 @@ "description": "How many asks of the image build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.", "pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*", "default": "shanks=3,ace=2,g14=0,novox=0" - }, - "cpu-cap": { - "type": "string", - "description": "The most of each node's processors this module's builds may take, as node=percent pairs separated by commas, such as shanks=50,ace=75 (novox/hq issue 1806); a node not named takes 50. The node-engine holds the module's runtime to it as a CPU quota, at idle priority, and each build step starts no more threads than its part of it.", - "pattern": "|[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?(,[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?)*", - "default": "shanks=50,g14=50,ace=75" } }, "tools": [ @@ -83,7 +77,7 @@ "path": "${dir:state}/settings.json", "mode": "0600", "owner": "mesh-build-image", - "takes-config": ["capacity", "cpu-cap"], + "takes-config": ["capacity"], "merge": "json", "template": "files/settings.json" }, diff --git a/modules/mesh-build-node/cmd/mesh-build-node/cpu.go b/modules/mesh-build-node/cmd/mesh-build-node/cpu.go index 5221728a..a0b18454 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/cpu.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/cpu.go @@ -5,14 +5,15 @@ import ( "git.novox.be/novox/mesh-sdk/go/buildstep" ) -// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to -// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that -// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's -// throttle. +// The processors a step may take (novox/hq ADR 0553, issue 1806). The node-engine holds this module's runtime, with +// every other build module's of the node, in one slice at idle priority under the node's one share of its processors; +// inside that bound a step starts no more threads than its part of it: the share divided by every build slot of the +// node, from the words the composer gives (buildstep.NodeLimits), so the steps running at once do not wait in the +// kernel's throttle. -// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's -// capacity, read at every call like the capacity, so a change takes effect at the next step. -func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { +// cpuLimits is what one step is held to now. Without the composer's words, it assumes every build seat of the node +// beside it at this module's own capacity. +func (m *Module) cpuLimits() buildcpu.Limits { slots := 1 switch { case m.Capacity != nil: @@ -20,20 +21,17 @@ func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits { case m.Settings != "": slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)() } - if slots < 1 { - slots = 1 - } - return buildstep.NodeLimits(m.Settings, m.Node, slots, say) + return buildstep.NodeLimits(slots) } // threadEnv is the environment every command of a step is given for its threads: GOMAXPROCS for esbuild and every Go // program, UV_THREADPOOL_SIZE for Node's worker pool. func (m *Module) threadEnv() []string { - return buildcpu.Env(m.cpuLimits(nil).Threads) + return buildcpu.Env(m.cpuLimits().Threads) } // cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime -// to, read from its cgroup rather than assumed. +// to (its unit and its slice), read from its cgroup rather than assumed. func (m *Module) cpuStatus() map[string]any { - return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()} + return map[string]any{"limits": m.cpuLimits(), "in-force": buildcpu.ReadInForce()} } diff --git a/modules/mesh-build-node/files/settings.json b/modules/mesh-build-node/files/settings.json index 31e080c2..3175fe39 100644 --- a/modules/mesh-build-node/files/settings.json +++ b/modules/mesh-build-node/files/settings.json @@ -1,4 +1,3 @@ { - "capacity": "shanks=3,ace=2,g14=0,novox=0", - "cpu-cap": "shanks=50,g14=50,ace=75" + "capacity": "shanks=3,ace=2,g14=0,novox=0" } diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index a7882138..a13058a1 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -3,7 +3,7 @@ module meshbuildnode go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-node/go.sum b/modules/mesh-build-node/go.sum index 3a53c941..79aabaac 100644 --- a/modules/mesh-build-node/go.sum +++ b/modules/mesh-build-node/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2 h1:DugsX7h46z+1FOiNhtDE43Vf1WInz2vgwi0mn/1VZDg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026124909-76deab6c67c2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1 h1:6O9WKnlVR1uHV5DDQueHGbMAQzfVS06J7DBKjUPBG4E= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132005-852ba81c5af1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index dace2a37..993fb7a1 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -25,12 +25,6 @@ "description": "How many asks of the Node build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.", "pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*", "default": "shanks=3,ace=2,g14=0,novox=0" - }, - "cpu-cap": { - "type": "string", - "description": "The most of each node's processors this module's builds may take, as node=percent pairs separated by commas, such as shanks=50,ace=75 (novox/hq issue 1806); a node not named takes 50. The node-engine holds the module's runtime to it as a CPU quota, at idle priority, and each build step starts no more threads than its part of it.", - "pattern": "|[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?(,[a-z0-9_-]+=([1-9]|[1-9][0-9]|100)%?)*", - "default": "shanks=50,g14=50,ace=75" } }, "tools": [ @@ -105,7 +99,7 @@ "path": "${dir:state}/settings.json", "mode": "0600", "owner": "mesh-build-node", - "takes-config": ["capacity", "cpu-cap"], + "takes-config": ["capacity"], "merge": "json", "template": "files/settings.json" }, -- 2.54.0 From 3def293d947c8073654639d3092ce906d40a6c9e Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 14:41:12 +0100 Subject: [PATCH 4/4] Build modules: no thread limit without the composer's share; with it, the share over their own capacity (ADR 0553 round 2) Change A rebuilds the three build modules, so their runtimes restart once; it changes nothing else about their builds until change B gives them the share. --- modules/mesh-build-go/cmd/mesh-build-go/cpu.go | 13 ++++++------- .../cmd/mesh-build-go/installed_toolchain_test.go | 14 ++++++-------- modules/mesh-build-go/go.mod | 2 +- modules/mesh-build-go/go.sum | 4 ++-- .../cmd/mesh-build-image/build_test.go | 3 ++- .../mesh-build-image/cmd/mesh-build-image/cpu.go | 13 ++++++------- modules/mesh-build-image/go.mod | 2 +- modules/mesh-build-image/go.sum | 4 ++-- modules/mesh-build-node/cmd/mesh-build-node/cpu.go | 13 ++++++------- .../cmd/mesh-build-node/fetch_test.go | 3 ++- modules/mesh-build-node/go.mod | 2 +- modules/mesh-build-node/go.sum | 4 ++-- 12 files changed, 37 insertions(+), 40 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/cpu.go b/modules/mesh-build-go/cmd/mesh-build-go/cpu.go index dc7c2a55..24457cb4 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/cpu.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/cpu.go @@ -6,13 +6,12 @@ import ( ) // The processors a step may take (novox/hq ADR 0553, issue 1806). The node-engine holds this module's runtime, with -// every other build module's of the node, in one slice at idle priority under the node's one share of its processors; -// inside that bound a step starts no more threads than its part of it: the share divided by every build slot of the -// node, from the words the composer gives (buildstep.NodeLimits), so the steps running at once do not wait in the -// kernel's throttle. +// every other build module's of the node, in one slice at idle priority under the node's one share of its processors: +// that quota bounds the total. Threads only size each step: the share the composer gives (buildstep.NodeLimits) over +// this module's own capacity, so a step does not wait in the kernel's throttle. Without the composer's share there is +// no slice to size a step to, and no thread limit is set. -// cpuLimits is what one step is held to now. Without the composer's words, it assumes every build seat of the node -// beside it at this module's own capacity. +// cpuLimits is what one step is held to now. func (m *Module) cpuLimits() buildcpu.Limits { slots := 1 switch { @@ -24,7 +23,7 @@ func (m *Module) cpuLimits() buildcpu.Limits { return buildstep.NodeLimits(slots) } -// threadEnv is the environment every go command of a step is given for its threads: GOMAXPROCS sets its -p, the compiler's +// threadEnv is the environment (nothing without a share) every go command of a step is given for its threads: GOMAXPROCS sets its -p, the compiler's // concurrency, a test binary's processors and -parallel. func (m *Module) threadEnv() []string { return buildcpu.Env(m.cpuLimits().Threads) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go b/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go index a498c044..5684567e 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/installed_toolchain_test.go @@ -31,6 +31,7 @@ func toolchainValues(env []string) []string { // 1572/AC1 (ADR 0477 §1): every environment a step gives its go commands says GOTOOLCHAIN=local, once, and names no // release. func TestEveryStepEnvironmentSaysGotoolchainLocal(t *testing.T) { + t.Setenv("MESH_BUILD_CPU_SHARE", "50") m := &Module{Cache: t.TempDir()} vend := t.TempDir() os.MkdirAll(filepath.Join(vend, "vendor"), 0o755) @@ -49,18 +50,15 @@ func TestEveryStepEnvironmentSaysGotoolchainLocal(t *testing.T) { t.Errorf("%s: GOMAXPROCS said %d times, want once: %v", name, procs, env) } } - // Three slots of its own and no words from the composer: every build seat beside it at three, at the default - // share; with the composer's words, its part of the node's share (ADR 0553 §4). + // Without the composer's share no thread limit; with it, the share over its own three slots (ADR 0553 §4). t.Setenv("MESH_BUILD_CPU_SHARE", "") - t.Setenv("MESH_BUILD_SLOTS", "") m = &Module{Cache: t.TempDir(), Node: "shanks", Capacity: func() int { return 3 }} - if l := m.cpuLimits(); l.Percent != 50 || l.Slots != 9 || l.Threads != buildcpu.Threads(50, runtime.NumCPU(), 9) { - t.Errorf("without words: %+v", l) + if l := m.cpuLimits(); l.Threads != 0 || strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS=") { + t.Errorf("without the share: %+v, goEnv %v", l, m.goEnv()) } t.Setenv("MESH_BUILD_CPU_SHARE", "50") - t.Setenv("MESH_BUILD_SLOTS", "4") - want := buildcpu.Threads(50, runtime.NumCPU(), 4) - if l := m.cpuLimits(); l.Slots != 4 || !strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS="+strconv.Itoa(want)) { + want := buildcpu.Threads(50, runtime.NumCPU(), 3) + if l := m.cpuLimits(); l.Slots != 3 || !strings.Contains(strings.Join(m.goEnv(), " "), "GOMAXPROCS="+strconv.Itoa(want)) { t.Errorf("limits %+v, goEnv %v; want %d threads", l, m.goEnv(), want) } if st := m.cpuStatus(); st["limits"].(buildcpu.Limits) != m.cpuLimits() { diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index cf6a2166..ba9c1c0e 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index b6979a51..f87f866a 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207 h1:up88FwKjL9hfVbwMv9THSLOMzk7Rv1Cr02ZqjUzx4XY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2 h1:wyOoONFYpcgLr2IueawZHyRM8vogkltXNrjgajGo3hM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-image/cmd/mesh-build-image/build_test.go b/modules/mesh-build-image/cmd/mesh-build-image/build_test.go index d146bfab..04fad85a 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/build_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/build_test.go @@ -69,6 +69,7 @@ func setup(t *testing.T, edit func(string) string) (*Module, *fake, *keptStates) // Dockerfile of the module's own directory and that directory as the context, the target, the pinned base as a build // argument; no mount, no credential file, a runtime directory of its own, and it answers the image's digest. func TestBuildRunsBuildahRootlessAndAnswersTheDigest(t *testing.T) { + t.Setenv("MESH_BUILD_CPU_SHARE", "50") // the composer's share (ADR 0553): buildah is given its threads m, f, states := setup(t, same) res, st := runBuild(t, m, states, nil) if st.State != delivery.StepPassed { @@ -111,7 +112,7 @@ func TestBuildRunsBuildahRootlessAndAnswersTheDigest(t *testing.T) { t.Errorf("the environment of %q lacks the module's own runtime directory: %s", c.line, env) } if !strings.Contains(env, "GOMAXPROCS=") { - t.Errorf("the environment of %q gives buildah no thread limit (issue 1806): %s", c.line, env) + t.Errorf("the environment of %q gives buildah no thread limit under a share (ADR 0553): %s", c.line, env) } } // The image is removed from the store once the digest is read, so the store does not grow with every build. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/cpu.go b/modules/mesh-build-image/cmd/mesh-build-image/cpu.go index 1e253ff6..126b319d 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/cpu.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/cpu.go @@ -6,13 +6,12 @@ import ( ) // The processors a step may take (novox/hq ADR 0553, issue 1806). The node-engine holds this module's runtime, with -// every other build module's of the node, in one slice at idle priority under the node's one share of its processors; -// inside that bound a step starts no more threads than its part of it: the share divided by every build slot of the -// node, from the words the composer gives (buildstep.NodeLimits), so the steps running at once do not wait in the -// kernel's throttle. +// every other build module's of the node, in one slice at idle priority under the node's one share of its processors: +// that quota bounds the total. Threads only size each step: the share the composer gives (buildstep.NodeLimits) over +// this module's own capacity, so a step does not wait in the kernel's throttle. Without the composer's share there is +// no slice to size a step to, and no thread limit is set. -// cpuLimits is what one step is held to now. Without the composer's words, it assumes every build seat of the node -// beside it at this module's own capacity. +// cpuLimits is what one step is held to now. func (m *Module) cpuLimits() buildcpu.Limits { slots := 1 switch { @@ -24,7 +23,7 @@ func (m *Module) cpuLimits() buildcpu.Limits { return buildstep.NodeLimits(slots) } -// threadEnv is the environment buildah is given for its threads (it is a Go program). What a build's RUN lines start +// threadEnv is the environment (nothing without a share) buildah is given for its threads (it is a Go program). What a build's RUN lines start // runs in the container's own environment, inside the runtime's cgroup: the slice's quota and the nice level bind it. func (m *Module) threadEnv() []string { return buildcpu.Env(m.cpuLimits().Threads) diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index e3f3e5e2..1a678ce3 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -3,7 +3,7 @@ module meshbuildimage go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-image/go.sum b/modules/mesh-build-image/go.sum index b6074ee6..bc2822f4 100644 --- a/modules/mesh-build-image/go.sum +++ b/modules/mesh-build-image/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207 h1:up88FwKjL9hfVbwMv9THSLOMzk7Rv1Cr02ZqjUzx4XY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2 h1:wyOoONFYpcgLr2IueawZHyRM8vogkltXNrjgajGo3hM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-build-node/cmd/mesh-build-node/cpu.go b/modules/mesh-build-node/cmd/mesh-build-node/cpu.go index a0b18454..b40ceda4 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/cpu.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/cpu.go @@ -6,13 +6,12 @@ import ( ) // The processors a step may take (novox/hq ADR 0553, issue 1806). The node-engine holds this module's runtime, with -// every other build module's of the node, in one slice at idle priority under the node's one share of its processors; -// inside that bound a step starts no more threads than its part of it: the share divided by every build slot of the -// node, from the words the composer gives (buildstep.NodeLimits), so the steps running at once do not wait in the -// kernel's throttle. +// every other build module's of the node, in one slice at idle priority under the node's one share of its processors: +// that quota bounds the total. Threads only size each step: the share the composer gives (buildstep.NodeLimits) over +// this module's own capacity, so a step does not wait in the kernel's throttle. Without the composer's share there is +// no slice to size a step to, and no thread limit is set. -// cpuLimits is what one step is held to now. Without the composer's words, it assumes every build seat of the node -// beside it at this module's own capacity. +// cpuLimits is what one step is held to now. func (m *Module) cpuLimits() buildcpu.Limits { slots := 1 switch { @@ -24,7 +23,7 @@ func (m *Module) cpuLimits() buildcpu.Limits { return buildstep.NodeLimits(slots) } -// threadEnv is the environment every command of a step is given for its threads: GOMAXPROCS for esbuild and every Go +// threadEnv is the environment (nothing without a share) every command of a step is given for its threads: GOMAXPROCS for esbuild and every Go // program, UV_THREADPOOL_SIZE for Node's worker pool. func (m *Module) threadEnv() []string { return buildcpu.Env(m.cpuLimits().Threads) diff --git a/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go b/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go index a976fb92..01f2c376 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/fetch_test.go @@ -226,6 +226,7 @@ func mustReadDir(t *testing.T, dir string) []string { // are given PATH, HOME and the locale and their own words; and the real clean runner, run on `env` here, prints only // those. func TestNoCommandOfTheStepSeesATokenOfTheProcess(t *testing.T) { + t.Setenv("MESH_BUILD_CPU_SHARE", "50") t.Setenv("MESH_BUS_CREDENTIAL", "hunter2") t.Setenv("NPM_TOKEN", "hunter3") files := typeScriptModule(`{"dependencies":{"lodash":"^4"}}`) @@ -246,7 +247,7 @@ func TestNoCommandOfTheStepSeesATokenOfTheProcess(t *testing.T) { if len(c.env) < 3 || !strings.HasPrefix(c.env[0], "PATH=") || !strings.HasPrefix(c.env[1], "HOME=") || c.env[2] != "LC_ALL=C" { t.Errorf("%s was given %v, want PATH, HOME and the locale first", c.name, c.env) } - // And its threads, its part of the node's processor cap (issue 1806). + // And its threads, its part of the node's share the composer gave (ADR 0553). if !slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "GOMAXPROCS=") }) || !slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "UV_THREADPOOL_SIZE=") }) { t.Errorf("%s was given %v, without its threads", c.name, c.env) diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index 437bb31f..d9b65d03 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -3,7 +3,7 @@ module meshbuildnode go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-node/go.sum b/modules/mesh-build-node/go.sum index 3d3f2971..90f01ef5 100644 --- a/modules/mesh-build-node/go.sum +++ b/modules/mesh-build-node/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207 h1:up88FwKjL9hfVbwMv9THSLOMzk7Rv1Cr02ZqjUzx4XY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026132226-36c92861d207/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2 h1:wyOoONFYpcgLr2IueawZHyRM8vogkltXNrjgajGo3hM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026133939-527d87613eb2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= -- 2.54.0