From 6250b1274b3d8179ca36dcbfb346722e09e3c1ae Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 13:37:48 +0100 Subject: [PATCH] A process that yields is written at idle priority under its share of the machine (issue 1806) A build seat's runtime took 1650% of shanks' 24 processors and slowed the operator's desktop. The unit of a process declared with yields now carries CPUWeight=idle, IOWeight=1, Nice=19, the lowest best-effort IO level, and CPUQuota for its share of the processors this machine has. --- go.mod | 2 +- go.sum | 16 ++-------------- internal/apply/process.go | 32 ++++++++++++++++++++++++++++++++ internal/apply/process_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index a5334da..84dff98 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 + 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 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 976c4b4..08be197 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024234636-10b6e202320f h1:lUEyNPPF3w4Q0LIgcUUD6ofkKVhCF0n7dVjMoixQmXo= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024234636-10b6e202320f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025204213-f63fa86c7456 h1:He2o9vSKjgKbWOCOgOJY0WlaDh1n8RcGhvGY/dwXsBQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025204213-f63fa86c7456/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025204730-0be0ffd6159e h1:k/1NjjW5HuuuHW2ldyWTvjp+1u3fN0RO18Q57Iup6Dk= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025204730-0be0ffd6159e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026105311-175a5567b8bd h1:ef6y09csev6xY/suFrIFGUXbgvDA29kummFtHcIb3B8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026105311-175a5567b8bd/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee h1:ZdBMkF4WYJi82jb3xYi9Z2lXN/iMTn9r3SFC122ROY4= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112307-af66adc180cd h1:DV4xfPRqmh6p20D2QNN+9EGFAyvpGOZPTRsSbpT7oww= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112307-af66adc180cd/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/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/internal/apply/process.go b/internal/apply/process.go index cbf4e06..c1a89a0 100644 --- a/internal/apply/process.go +++ b/internal/apply/process.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "time" @@ -277,6 +278,7 @@ func unitFor(r *declaration.Process) string { if r.User != "" { fmt.Fprintf(&b, "User=%s\n", r.User) } + writeYields(&b, r.Yields) fmt.Fprintf(&b, "ExecStart=%s\n", strings.Join(runFrom(r), " ")) if r.Schedule != "" || r.RunOnce { // Started by its timer, or once by the host, and expected to finish. Restarting it would have it run @@ -293,6 +295,36 @@ func unitFor(r *declaration.Process) string { return b.String() } +// machineCPUs is how many processors this machine has, for a share of them in a quota. A variable only so a test can +// fix it. +var machineCPUs = runtime.NumCPU + +// writeYields writes how a process gives way to everything else on its machine (novox/hq issue 1806): a build seat's +// holder, whose builds must never slow the operator's desktop. +// +// **Idle priority** is four lines, because one alone leaves a door open: CPUWeight=idle runs its cgroup only on cycles +// its siblings leave, Nice=19 holds every process it starts below the machine's own within the group, IOWeight=1 and +// the lowest best-effort IO level put its disk work behind everyone's. Not the idle IO class: a disk that is never idle +// would starve a build for good, and a build that never ends holds a check for ever. +// +// **The share** is a hard bound: CPUQuota counts in percent of one processor, so a share of the machine is written for +// the processors it has (half of 24 is 1200%), at least one processor. +func writeYields(b *strings.Builder, y *declaration.Yields) { + if y == nil { + return + } + if y.Priority == declaration.YieldIdle { + b.WriteString("CPUWeight=idle\nIOWeight=1\nNice=19\nIOSchedulingClass=best-effort\nIOSchedulingPriority=7\n") + } + if y.CPUShare > 0 { + quota := y.CPUShare * machineCPUs() + if quota < 100 { + quota = 100 + } + fmt.Fprintf(b, "CPUQuota=%d%%\n", quota) + } +} + // timerFor is the cadence a scheduled process runs on. // // **The mesh's cron expression, handed to the machine's own timer.** The host already parses and diff --git a/internal/apply/process_test.go b/internal/apply/process_test.go index 20b1cec..f341e0c 100644 --- a/internal/apply/process_test.go +++ b/internal/apply/process_test.go @@ -83,6 +83,36 @@ func TestAProcessRunsAsWhoItSaid(t *testing.T) { if strings.Contains(unitFor(aProcess()), "User=") { t.Fatalf("a process that named no user had one written for it:\n%s", unitFor(aProcess())) } + + // A process that yields (issue 1806) is written at idle priority under its share of the machine's processors; one + // that does not has none of it. + defer func(n func() int) { machineCPUs = n }(machineCPUs) + for _, c := range []struct { + cpus int + y *declaration.Yields + want []string + never []string + }{ + {24, &declaration.Yields{Priority: declaration.YieldIdle, CPUShare: 50}, + []string{"CPUWeight=idle\n", "IOWeight=1\n", "Nice=19\n", "IOSchedulingPriority=7\n", "CPUQuota=1200%\n"}, nil}, + {1, &declaration.Yields{CPUShare: 25}, []string{"CPUQuota=100%\n"}, []string{"Nice="}}, + {24, nil, nil, []string{"CPUWeight=", "Nice=", "CPUQuota=", "IOWeight="}}, + } { + machineCPUs = func() int { return c.cpus } + p := aProcess() + p.Yields = c.y + unit := unitFor(p) + for _, w := range c.want { + if !strings.Contains(unit, w) { + t.Errorf("yields %+v on %d processors: the unit does not say %q:\n%s", c.y, c.cpus, w, unit) + } + } + for _, n := range c.never { + if strings.Contains(unit, n) { + t.Errorf("yields %+v: the unit says %q:\n%s", c.y, n, unit) + } + } + } } // **Three modes, one kind.** A scheduled process is a timer plus a unit that finishes, not a unit