mesh/delivery-group group fix/capacity-follows-cpu-share rejected: composed together: the checker's verdict at the members' heads (63694fe9222d): unit: ..test
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: ..test: ✔ a transient empty list for four passes retires nothing; five stable passes retire, data kept (418.253369ms) | d00da…
The operator, 2026-10-26 21:18: g14 builds only from 18:00 to 06:00. A build-hours setting names a window per node in its local time (crossing midnight allowed, absent = any hour); outside it the capacity judged by is 0, running steps finish, and the puller wakes on one timer at the next edge. The change is said in the log and in capacity.<node> with the window and when it opens.
138 lines
6.0 KiB
Go
138 lines
6.0 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// ADR 0553 §4: a step's threads are the composer's share over the module's own capacity; without the word, none. And
|
|
// issue 1806's follow-up: the capacity judged by is the configured one bounded by the build slice's cores over the cores
|
|
// a step needs (the three build nodes as they stand on 2026-10-26), and cores-per-step is read from the settings file.
|
|
func TestNodeLimitsAndTheCPUBound(t *testing.T) {
|
|
t.Setenv("MESH_BUILD_CPU_SHARE", "")
|
|
if l := NodeLimits(3); l.Threads != 0 || l.Slots != 3 {
|
|
t.Errorf("without the word: %+v", l)
|
|
}
|
|
t.Setenv("MESH_BUILD_CPU_SHARE", "75")
|
|
if l := NodeLimits(3); l.Percent != 75 || l.Slots != 3 || l.Threads < 1 {
|
|
t.Errorf("from the word: %+v", l)
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
configured, quota int
|
|
cores float64
|
|
effective, byCPU int
|
|
}{
|
|
{"shanks: 12 cores, configured 3", 3, 1200, 6, 2, 2},
|
|
{"ace: 18 cores, configured 2", 2, 1800, 6, 2, 3},
|
|
{"g14: 8 cores, configured 2", 2, 800, 6, 1, 1},
|
|
{"node on shanks: 12 cores at 2", 3, 1200, 2, 3, 6},
|
|
{"no quota: the configured one", 3, 0, 6, 3, 0},
|
|
{"configured 0 pulls nothing", 0, 1200, 6, 0, 0},
|
|
{"a quota under one step still builds one", 2, 100, 6, 1, 1},
|
|
{"no cores a step is one", 2, 150, 0, 1, 1},
|
|
} {
|
|
got := EffectiveCapacity(c.configured, c.quota, "test", c.cores)
|
|
if got.Effective != c.effective || got.ByCPU != c.byCPU || got.Why == "" {
|
|
t.Errorf("%s: %+v, want effective %d, by CPU %d", c.name, got, c.effective, c.byCPU)
|
|
}
|
|
}
|
|
file := filepath.Join(t.TempDir(), "settings.json")
|
|
var said []string
|
|
cores := NodeCoresPerStep(file, 6, func(f string, a ...any) { said = append(said, f) })
|
|
for _, c := range []struct {
|
|
file string
|
|
want float64
|
|
}{{`{"capacity":"shanks=2"}`, 6}, {`{"cores-per-step":4}`, 4}, {`{"cores-per-step":"3.5"}`, 3.5}, {`{"cores-per-step":0}`, 6}, {`{"cores-per-step":0}`, 6}} {
|
|
os.WriteFile(file, []byte(c.file), 0o600)
|
|
if got := cores(); got != c.want {
|
|
t.Errorf("cores-per-step from %s = %g, want %g", c.file, got, c.want)
|
|
}
|
|
}
|
|
if len(said) != 1 {
|
|
t.Errorf("a value not above zero is said once, said %d times", len(said))
|
|
}
|
|
|
|
// The operator, 2026-10-26 21:18: g14 builds only from 18:00 to 06:00, its local time. Outside its build hours the
|
|
// capacity judged by is 0, said with the window and when it opens; a node not named, or a bad value, builds at any hour.
|
|
cet := time.FixedZone("CET", 3600)
|
|
day := func(hh, mm int) time.Time { return time.Date(2026, 10, 26, hh, mm, 0, 0, cet) }
|
|
was := now
|
|
defer func() { now = was }()
|
|
for _, c := range []struct {
|
|
name, setting, node string
|
|
at time.Time
|
|
effective int
|
|
edge string // the window's next edge, HH:MM; "" for no window
|
|
bad bool
|
|
}{
|
|
{"g14 in the office", "g14=18:00-06:00", "g14", day(10, 0), 0, "18:00", false},
|
|
{"g14 in the evening", "g14=18:00-06:00", "g14", day(18, 0), 2, "06:00", false},
|
|
{"g14 past midnight", "g14=18:00-06:00", "g14", day(5, 59), 2, "06:00", false},
|
|
{"g14 at the close", "g14=18:00-06:00", "g14", day(6, 0), 0, "18:00", false},
|
|
{"a day window", "shanks=09:00-17:30,g14=18:00-06:00", "shanks", day(17, 29), 2, "17:30", false},
|
|
{"a node not named", "g14=18:00-06:00", "ace", day(10, 0), 2, "", false},
|
|
{"a bad time", "g14=18:00-6", "g14", day(10, 0), 2, "", true},
|
|
{"no span", "g14=18:00-18:00", "g14", day(10, 0), 2, "", true},
|
|
} {
|
|
w, err := ParseBuildHours(c.setting, c.node)
|
|
if (err != nil) != c.bad {
|
|
t.Errorf("%s: parse %v", c.name, err)
|
|
}
|
|
if edge := ""; w != nil {
|
|
edge = w.NextEdge(c.at).Format("15:04")
|
|
if edge != c.edge {
|
|
t.Errorf("%s: next edge %s, want %s", c.name, edge, c.edge)
|
|
}
|
|
} else if c.edge != "" {
|
|
t.Errorf("%s: no window, want one", c.name)
|
|
}
|
|
file := filepath.Join(t.TempDir(), "settings.json")
|
|
os.WriteFile(file, []byte(`{"build-hours":"`+c.setting+`"}`), 0o600)
|
|
now = func() time.Time { return c.at }
|
|
p := &Puller{Language: "go", Node: c.node, Capacity: func() int { return 2 }, BuildHours: NodeBuildHours(file, c.node, nil)}
|
|
got := p.CapacityDetail()
|
|
if got.Effective != c.effective || got.OutsideHours != (c.effective == 0) || (c.effective == 0 && !strings.Contains(got.Why, "outside its build hours 18:00-06:00 CET until 18:00")) {
|
|
t.Errorf("%s: %+v, want capacity %d", c.name, got, c.effective)
|
|
}
|
|
// A value that cannot be read builds at any hour, loudly: the record names the value and says why.
|
|
if c.bad && (!strings.Contains(got.BuildHoursFault, c.setting) || !strings.Contains(got.Why, "at any hour, because its build hours cannot be read")) {
|
|
t.Errorf("%s: a bad value is not said in the record: %+v", c.name, got)
|
|
}
|
|
}
|
|
}
|