Merge pull request 'The composer gives build modules the node's share: idle, capped builds (issue 1806, ADR 0553/0554, change B)' (#95) from fix/build-cpu-yields-compose into main
This commit was merged in pull request #95.
This commit is contained in:
@@ -311,6 +311,17 @@ func (r Resolution) ownRuntimes(with Rendering) ([]map[string]any, error) {
|
|||||||
if words, err = r.readsWords(m, words, with); err != nil {
|
if words, err = r.readsWords(m, words, with); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// A build seat's holder gives way on its node, and its steps are told their part of the node's share
|
||||||
|
// (novox/hq ADR 0553).
|
||||||
|
yielding, yieldWords := r.yieldsOf(m, with)
|
||||||
|
if len(yieldWords) > 0 {
|
||||||
|
if words == nil {
|
||||||
|
words = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range yieldWords {
|
||||||
|
words[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(words) > 0 {
|
if len(words) > 0 {
|
||||||
body, err := json.Marshal(map[string]map[string]string{m.Module: words})
|
body, err := json.Marshal(map[string]map[string]string{m.Module: words})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -329,6 +340,9 @@ func (r Resolution) ownRuntimes(with Rendering) ([]map[string]any, error) {
|
|||||||
process[HealthField] = m.RuntimeHealth
|
process[HealthField] = m.RuntimeHealth
|
||||||
healthInto(process, m, with)
|
healthInto(process, m, with)
|
||||||
}
|
}
|
||||||
|
if yielding != nil {
|
||||||
|
process["yields"] = yielding
|
||||||
|
}
|
||||||
if err := artifactsInto(process, RuntimeModule, with); err != nil {
|
if err := artifactsInto(process, RuntimeModule, with); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,6 +498,54 @@ func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) {
|
|||||||
if _, told := env[RuntimeOperatorAccount]; told {
|
if _, told := env[RuntimeOperatorAccount]; told {
|
||||||
t.Error("a runtime of a module's own account is told the operator's account")
|
t.Error("a runtime of a module's own account is told the operator's account")
|
||||||
}
|
}
|
||||||
|
if _, yields := own["yields"]; yields {
|
||||||
|
t.Errorf("telegram holds no build seat and yields: %v", own["yields"])
|
||||||
|
}
|
||||||
|
// The build seats' holders yield (ADR 0553): idle, under the node's one share from the node-engine's build-cpu-share,
|
||||||
|
// and their steps are told it.
|
||||||
|
builder := func(name, seat string) Manifest {
|
||||||
|
b := aToolsModule(t, name, "tools/index.js")
|
||||||
|
b.RunsAs, b.SecretsOwner = name, name
|
||||||
|
b.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/" + name + "/broker"}}
|
||||||
|
b.Claims = []Claim{{Name: seat}}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
gob, img := builder("gob", "node-build-go"), builder("img", "node-build-image")
|
||||||
|
with.Needed["gob"], with.Needed["img"] = map[string]string{"broker": "own"}, map[string]string{"broker": "own"}
|
||||||
|
for _, c := range []struct {
|
||||||
|
settings SettingsBy
|
||||||
|
share string
|
||||||
|
}{
|
||||||
|
{nil, "50"},
|
||||||
|
{SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(75)}}}}, "75"},
|
||||||
|
{SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(0)}}}}, "50"},
|
||||||
|
} {
|
||||||
|
with.Settings = c.settings
|
||||||
|
runtimes, err := Resolution{Node: "anchor", Modules: []Manifest{gob, img, telegram, goRuntime}}.ownRuntimes(with)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(runtimes) != 3 {
|
||||||
|
t.Fatalf("%d runtimes of their own, want gob's, img's and telegram's", len(runtimes))
|
||||||
|
}
|
||||||
|
for _, p := range runtimes {
|
||||||
|
name := fmt.Sprint(p["name"])
|
||||||
|
words := map[string]map[string]string{}
|
||||||
|
_ = json.Unmarshal([]byte(p["env"].(map[string]string)[RuntimeToolEnv]), &words)
|
||||||
|
mod := strings.TrimSuffix(name, "-runtime")
|
||||||
|
if mod == "telegram" {
|
||||||
|
if p["yields"] != nil || words[mod]["MESH_BUILD_CPU_SHARE"] != "" {
|
||||||
|
t.Errorf("telegram yields under %v: %v %v", c.settings, p["yields"], words)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got := fmt.Sprint(p["yields"]); got != "map[cpu-share:"+c.share+" priority:idle]" ||
|
||||||
|
words[mod]["MESH_BUILD_CPU_SHARE"] != c.share {
|
||||||
|
t.Errorf("%s under %v yields %s with words %v; want share %s", name, c.settings, got, words[mod], c.share)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with.Settings = nil
|
||||||
// Without the machine's runtime to run it from, it is refused in words.
|
// Without the machine's runtime to run it from, it is refused in words.
|
||||||
if _, err := (Resolution{Node: "anchor", Modules: []Manifest{telegram}}).ownRuntimes(with); err == nil {
|
if _, err := (Resolution{Node: "anchor", Modules: []Manifest{telegram}}).ownRuntimes(with); err == nil {
|
||||||
t.Error("a module of its own account composed without a runtime program")
|
t.Error("a module of its own account composed without a runtime program")
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/buildcpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
// yieldsOf is how the runtime of its own a module is served by gives way on this node (novox/hq ADR 0553), or nil when
|
||||||
|
// it does not, and the word its steps size their threads from:
|
||||||
|
//
|
||||||
|
// - a module holding a seat that yields (Seat.Yields, the build seats) runs at idle priority under the node's one share
|
||||||
|
// of its processors: the node-engine's own config build-cpu-share (buildcpu.ShareSetting) as this node's layers give
|
||||||
|
// it, its default where none does. Every yielding module of the node carries the same share, so the node-engine's
|
||||||
|
// one yielding slice has one quota (ADR 0553 §2, §3).
|
||||||
|
// - its word: that share (buildcpu.ShareWord), which a step divides by its own module's capacity (ADR 0553 §4).
|
||||||
|
//
|
||||||
|
// Never a refusal: a share out of range is the default, so a mistyped value bounds the builds at the default and never
|
||||||
|
// stops the node's whole composition.
|
||||||
|
func (r Resolution) yieldsOf(m Manifest, with Rendering) (map[string]any, map[string]string) {
|
||||||
|
if !yields(m) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
engine := Manifest{Module: engineModule}
|
||||||
|
for _, e := range r.Modules {
|
||||||
|
if e.Module == engineModule {
|
||||||
|
engine = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
share := buildcpu.DefaultPercent
|
||||||
|
for _, s := range Effective(engine, with.Settings[engineModule]) {
|
||||||
|
if s.Key == buildcpu.ShareSetting {
|
||||||
|
if n, ok := percentOf(s.Value); ok {
|
||||||
|
share = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]any{"priority": "idle", "cpu-share": share},
|
||||||
|
map[string]string{buildcpu.ShareWord: strconv.Itoa(share)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// yields is whether a module claims a seat that yields.
|
||||||
|
func yields(m Manifest) bool {
|
||||||
|
for _, c := range m.Claims {
|
||||||
|
if s, ok := SeatNamed(c.Name); ok && s.Yields {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// percentOf is a setting's value as a percent from 1 to 100: a number, or a string holding one.
|
||||||
|
func percentOf(v any) (int, bool) {
|
||||||
|
var n int
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
n = int(x)
|
||||||
|
case int:
|
||||||
|
n = x
|
||||||
|
case string:
|
||||||
|
var err error
|
||||||
|
if n, err = strconv.Atoi(x); err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return n, n >= 1 && n <= 100
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user