A build seat's capacity follows its node's build CPU, and a running ask says its phases

Since the build CPU cap (issue 1806) shanks' mesh-build-go kept capacity 3 inside a 12-core slice:
three full checks got about 4 cores each and ran past the 30-minute step bound (issue 1890).
The puller now judges by min(configured, floor(slice quota cores / cores-per-step)), the quota
read live from its cgroup, says it on change and writes capacity.<node> to the pauses state.
A run's phases (fetch and merge, digest, tests) are logged with the ask id and written to its
running step key; Asker.Wait hands every step value to a WithStepSeen hook.
This commit is contained in:
2026-10-26 19:52:54 +01:00
parent a7a1fbd34e
commit 02f50bd9d6
11 changed files with 500 additions and 22 deletions
+51
View File
@@ -693,3 +693,54 @@ func DecodePause(data []byte) (Pause, error) {
}
return p, nil
}
// CapacityKey is the key, in a build module's PausesState, where the module on node says the capacity it judges by
// now and why: `capacity.<node>` (issue 1806's follow-up). The pauses state has no age, so the value stands until the
// module writes another; a reader such as the delivery view's build_usage says it in place of the configured capacity.
func CapacityKey(node string) (string, error) {
if err := checkName("node", node); err != nil {
return "", err
}
return "capacity." + node, nil
}
// Capacity is a build seat's capacity on one node as its holder judges by it: the configured capacity (the module's
// setting `capacity`), bounded by the cores its node's build slice gives (its CPU quota, read live) over the cores one
// step needs (the module's setting `cores-per-step`). Effective is min(Configured, ByCPU) when a quota is known, else
// Configured; one step at least while Configured is above zero, so a node whose quota is under one step's need still
// builds, slowly.
type Capacity struct {
V int `json:"v"`
Effective int `json:"effective"`
Configured int `json:"configured"`
// ConfiguredFrom says where Configured was read: mesh-config's value as the config watch handed it, or the settings
// file.
ConfiguredFrom string `json:"configured-from,omitempty"`
// QuotaCores is the build slice's CPU quota in processors (cpu.max: 1200% is 12), 0 when none is known; QuotaFrom
// says where it was read (the cgroup, or the share the composer gave) or why none is known.
QuotaCores float64 `json:"quota-cores,omitempty"`
QuotaFrom string `json:"quota-from"`
// CoresPerStep is what one step of this module needs; ByCPU is floor(QuotaCores / CoresPerStep), at least 1, and 0
// when no quota is known.
CoresPerStep float64 `json:"cores-per-step"`
ByCPU int `json:"by-cpu,omitempty"`
// Why is the capacity in words: what bounds it.
Why string `json:"why"`
At time.Time `json:"at,omitzero"`
}
// EncodeCapacity is c as JSON at Version.
func EncodeCapacity(c Capacity) ([]byte, error) {
c.V = Version
c.At = c.At.UTC()
return json.Marshal(c)
}
// DecodeCapacity reads a capacity value; it refuses an unknown version (ErrUnknownVersion).
func DecodeCapacity(data []byte) (Capacity, error) {
var c Capacity
if err := decode(data, &c); err != nil {
return Capacity{}, err
}
return c, nil
}