Build seats yield to one shared slice per node; the composer half waits for the engines (ADR 0553, issue 1806)
Review round 1: the decoder type ships before any composer can emit it, so a routine SDK bump never sends yields to an engine that refuses them; the composer half moves to a later change. Every yielding process of a node goes into one top-level slice, meshyield.slice, so the three build modules share one quota instead of taking one each; the share is one node value, node-tools' build-cpu-share, and each step's threads divide it by every build slot of the node (words from the composer, a conservative fallback without them).
This commit is contained in:
@@ -4,77 +4,72 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The cap's words, a node's share, the unit quota and the threads a step is given, in one table (issue 1806).
|
||||
func TestCapQuotaAndThreads(t *testing.T) {
|
||||
// The node's share as a quota, the threads one step is given across every build slot of the node, and the words the
|
||||
// composer hands, in one table (ADR 0553, issue 1806).
|
||||
func TestShareQuotaAndThreads(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name, setting, node string
|
||||
cores, slots int
|
||||
percent, quota int
|
||||
threads int
|
||||
refused bool
|
||||
name string
|
||||
percent, cores, slots int
|
||||
quota, threads int
|
||||
}{
|
||||
{name: "default for shanks", setting: Defaults, node: "shanks", cores: 24, slots: 3, percent: 50, quota: 1200, threads: 4},
|
||||
{name: "default for ace", setting: Defaults, node: "ace", cores: 24, slots: 2, percent: 75, quota: 1800, threads: 9},
|
||||
{name: "default for g14", setting: Defaults, node: "g14", cores: 16, slots: 1, percent: 50, quota: 800, threads: 8},
|
||||
{name: "a node not named", setting: Defaults, node: "novox", cores: 8, slots: 1, percent: DefaultPercent, quota: 400, threads: 4},
|
||||
{name: "percent sign and spaces", setting: " shanks = 25% ", node: "shanks", cores: 24, slots: 3, percent: 25, quota: 600, threads: 2},
|
||||
{name: "one processor at least", setting: "tiny=10", node: "tiny", cores: 2, slots: 4, percent: 10, quota: 100, threads: 1},
|
||||
{name: "zero slots counts as one", setting: "x=100", node: "x", cores: 4, slots: 0, percent: 100, quota: 400, threads: 4},
|
||||
{name: "zero refused", setting: "shanks=0", node: "shanks", refused: true},
|
||||
{name: "over a hundred refused", setting: "shanks=150", node: "shanks", refused: true},
|
||||
{name: "no number refused", setting: "shanks", node: "shanks", refused: true},
|
||||
{name: "twice refused", setting: "a=1,a=2", node: "a", refused: true},
|
||||
{"shanks: half of 24 across 9 slots", 50, 24, 9, 1200, 1},
|
||||
{"shanks: half of 24 across 4 slots", 50, 24, 4, 1200, 3},
|
||||
{"ace: three quarters of 24 across 6 slots", 75, 24, 6, 1800, 3},
|
||||
{"g14: half of 16 across 1 slot", 50, 16, 1, 800, 8},
|
||||
{"one processor at least", 10, 2, 4, 100, 1},
|
||||
{"no slots counts as one", 100, 4, 0, 400, 4},
|
||||
{"a share out of range is the default", 0, 8, 1, 0, 4},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
p, err := Of(c.setting, c.node)
|
||||
if c.refused {
|
||||
if err == nil {
|
||||
t.Fatalf("Of(%q) = %d, want refused", c.setting, p)
|
||||
}
|
||||
return
|
||||
if c.quota != 0 {
|
||||
if q := Quota(c.percent, c.cores); q != c.quota {
|
||||
t.Errorf("%s: Quota = %d, want %d", c.name, q, c.quota)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p != c.percent {
|
||||
t.Fatalf("percent = %d, want %d", p, c.percent)
|
||||
}
|
||||
if q := Quota(p, c.cores); q != c.quota {
|
||||
t.Errorf("Quota = %d, want %d", q, c.quota)
|
||||
}
|
||||
if th := Threads(p, c.cores, c.slots); th != c.threads {
|
||||
t.Errorf("Threads = %d, want %d", th, c.threads)
|
||||
}
|
||||
})
|
||||
}
|
||||
if th := Threads(c.percent, c.cores, c.slots); th != c.threads {
|
||||
t.Errorf("%s: Threads = %d, want %d", c.name, th, c.threads)
|
||||
}
|
||||
}
|
||||
caps, _ := Parse("ace=75,shanks=50%")
|
||||
if got := Format(caps); got != "ace=75,shanks=50" {
|
||||
t.Errorf("Format = %q", got)
|
||||
words := map[string]string{ShareWord: "75", SlotsWord: "6"}
|
||||
l := FromWords(func(k string) string { return words[k] }, 2)
|
||||
if l.Percent != 75 || l.From != ShareWord || l.Slots != 6 || l.SlotsFrom != SlotsWord || l.Threads != Threads(75, runtime.NumCPU(), 6) {
|
||||
t.Errorf("from the words: %+v", l)
|
||||
}
|
||||
if got := WithEnv([]string{"A=1", "GOMAXPROCS=24"}, 3); !reflect.DeepEqual(got, []string{"A=1", "GOMAXPROCS=3", "UV_THREADPOOL_SIZE=3"}) {
|
||||
t.Errorf("WithEnv = %q", got)
|
||||
// Told nothing (or nonsense), a module assumes every build seat beside it at its own capacity, at the default share.
|
||||
words = map[string]string{ShareWord: "250", SlotsWord: "many"}
|
||||
if l := FromWords(func(k string) string { return words[k] }, 3); l.Percent != DefaultPercent || l.Slots != 3*BuildSeats || l.From != "default" {
|
||||
t.Errorf("without usable words: %+v", l)
|
||||
}
|
||||
if got := Env(0); !reflect.DeepEqual(got, []string{"GOMAXPROCS=1", "UV_THREADPOOL_SIZE=1"}) {
|
||||
t.Errorf("Env = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// What the kernel holds a process to is read from its cgroup and stat files, not assumed.
|
||||
// What the kernel holds a process to is read from its unit's and its slice's cgroup and its stat file, not assumed.
|
||||
func TestReadInForce(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cg := filepath.Join(dir, "cg")
|
||||
unit := filepath.Join(cg, "system.slice", "mesh-build-go-runtime.service")
|
||||
slice := filepath.Join(cg, "meshyield.slice")
|
||||
unit := filepath.Join(slice, "mesh-build-go-runtime.service")
|
||||
if err := os.MkdirAll(unit, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, v := range map[string]string{"cpu.idle": "1\n", "cpu.weight": "0\n", "cpu.max": "1200000 100000\n", "io.weight": "default 1\n"} {
|
||||
if err := os.WriteFile(filepath.Join(unit, name), []byte(v), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
for at, files := range map[string]map[string]string{
|
||||
slice: {"cpu.idle": "1\n", "cpu.weight": "0\n", "cpu.max": "1200000 100000\n", "io.weight": "default 1\n"},
|
||||
unit: {"cpu.idle": "0\n", "cpu.weight": "100\n", "cpu.max": "max 100000\n", "io.weight": "default 100\n"},
|
||||
} {
|
||||
for name, v := range files {
|
||||
if err := os.WriteFile(filepath.Join(at, name), []byte(v), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
self := filepath.Join(dir, "cgroup")
|
||||
stat := filepath.Join(dir, "stat")
|
||||
_ = os.WriteFile(self, []byte("0::/system.slice/mesh-build-go-runtime.service\n"), 0o644)
|
||||
_ = os.WriteFile(self, []byte("0::/meshyield.slice/mesh-build-go-runtime.service\n"), 0o644)
|
||||
// A command with a space and a parenthesis in it, then nice 19 in field 19.
|
||||
_ = os.WriteFile(stat, []byte("42 (mesh (build) go) S 1 42 42 0 -1 4194560 1 0 0 0 0 0 0 0 39 19 8 0 1 2 3\n"), 0o644)
|
||||
cgroupRoot, procSelfCgroup, procSelfStat = cg, self, stat
|
||||
@@ -83,12 +78,13 @@ func TestReadInForce(t *testing.T) {
|
||||
})
|
||||
|
||||
got := ReadInForce()
|
||||
want := InForce{Nice: 19, Cgroup: "/system.slice/mesh-build-go-runtime.service", CPUIdle: "1", CPUWeight: "0",
|
||||
CPUMax: "1200000 100000", Quota: 1200, IOWeight: "default 1"}
|
||||
want := InForce{Nice: 19, Cgroup: "/meshyield.slice/mesh-build-go-runtime.service",
|
||||
Unit: Group{Path: "/meshyield.slice/mesh-build-go-runtime.service", CPUIdle: "0", CPUWeight: "100",
|
||||
CPUMax: "max 100000", IOWeight: "default 100"},
|
||||
Slice: Group{Path: "/meshyield.slice", CPUIdle: "1", CPUWeight: "0", CPUMax: "1200000 100000",
|
||||
IOWeight: "default 1"},
|
||||
Quota: 1200}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ReadInForce = %+v\nwant %+v", got, want)
|
||||
}
|
||||
if q := quotaOf("max 100000"); q != 0 {
|
||||
t.Errorf("no quota read as %d", q)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user