carlos / platform Public

cgroup: size names (#329)

The four named sizes (small, standard, large, xlarge) as a literal table
with SizeByName, SizeOf and SizeNames. No behaviour changes: TierDefault,
TierChildren, SharesOf and HighOf are untouched. The rows are literals
so plan 3 can derive the tiers from the table without an initialization
cycle (Astra plan R1-10); a parser test pins that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ce6975b54a933ae7b1a6fdd6507909a7b55b23f8 parent 3ff2a78
2 files changed, +155 −0
  • internal/cgroup/sizes.go +54 −0
  • internal/cgroup/sizes_test.go +101 −0
diff --git a/internal/cgroup/sizes.go b/internal/cgroup/sizes.go
new file mode 100644
index 000000000..5c2d6aed3
--- /dev/null
+++ b/internal/cgroup/sizes.go
@@ -0,0 +1,54 @@
+package cgroup
+
+// Size is a named instance size (#329): a memory limit and the CPU and IO
+// shares that go with it. Like the tiers, sizes are PLATFORM-chosen; an
+// operator picks a name, never a number.
+type Size struct {
+ Name string
+ Limit Limit
+ Shares Shares
+}
+
+// Sizes is the table of named sizes, smallest first. Today only the console
+// validates against it; TierDefault, TierChildren and SharesOf still decide
+// what a box writes. Plan 3 makes this table the source of every number
+// (spec §5.1) and derives TierDefault, TierChildren and SharesOf from it,
+// which is why every row is a literal: a row built from them would then be
+// an initialization cycle. standard and large equal today's two tiers; the
+// test, not this initializer, holds them equal.
+var Sizes = []Size{
+ {Name: "small", Limit: Limit{MemoryMax: 256 << 20}, Shares: Shares{CPUWeight: 50, CPUQuotaPercent: 50, IOWeight: 50}},
+ {Name: "standard", Limit: Limit{MemoryMax: 512 << 20}, Shares: Shares{CPUWeight: 100, CPUQuotaPercent: 100, IOWeight: 100}},
+ {Name: "large", Limit: Limit{MemoryMax: 1 << 30}, Shares: Shares{CPUWeight: 100, CPUQuotaPercent: 200, IOWeight: 100}},
+ {Name: "xlarge", Limit: Limit{MemoryMax: 2 << 30}, Shares: Shares{CPUWeight: 200, CPUQuotaPercent: 400, IOWeight: 200}},
+}
+
+// SizeByName is the size called name, matched exactly (no case folding).
+func SizeByName(name string) (Size, bool) {
+ for _, s := range Sizes {
+ if s.Name == name {
+ return s, true
+ }
+ }
+ return Size{}, false
+}
+
+// SizeOf is the size whose memory limit is l, if any.
+func SizeOf(l Limit) (Size, bool) {
+ for _, s := range Sizes {
+ if s.Limit == l {
+ return s, true
+ }
+ }
+ return Size{}, false
+}
+
+// SizeNames is the size names in table order, for messages. The slice is
+// the caller's own.
+func SizeNames() []string {
+ names := make([]string, len(Sizes))
+ for i, s := range Sizes {
+ names[i] = s.Name
+ }
+ return names
+}
diff --git a/internal/cgroup/sizes_test.go b/internal/cgroup/sizes_test.go
new file mode 100644
index 000000000..595156b38
--- /dev/null
+++ b/internal/cgroup/sizes_test.go
@@ -0,0 +1,101 @@
+package cgroup
+
+import (
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "reflect"
+ "testing"
+)
+
+func TestSizesTable(t *testing.T) {
+ want := []Size{
+ {Name: "small", Limit: Limit{MemoryMax: 256 << 20}, Shares: Shares{CPUWeight: 50, CPUQuotaPercent: 50, IOWeight: 50}},
+ {Name: "standard", Limit: Limit{MemoryMax: 512 << 20}, Shares: Shares{CPUWeight: 100, CPUQuotaPercent: 100, IOWeight: 100}},
+ {Name: "large", Limit: Limit{MemoryMax: 1 << 30}, Shares: Shares{CPUWeight: 100, CPUQuotaPercent: 200, IOWeight: 100}},
+ {Name: "xlarge", Limit: Limit{MemoryMax: 2 << 30}, Shares: Shares{CPUWeight: 200, CPUQuotaPercent: 400, IOWeight: 200}},
+ }
+ if !reflect.DeepEqual(Sizes, want) {
+ t.Fatalf("Sizes = %+v\nwant %+v", Sizes, want)
+ }
+
+ // standard and large are today's tiers, number for number. The test
+ // holds them equal, not the initializer (the rows are literals).
+ if Sizes[1].Limit != TierDefault || Sizes[1].Shares != SharesOf(TierDefault) {
+ t.Errorf("standard = %+v, want TierDefault %+v with shares %+v", Sizes[1], TierDefault, SharesOf(TierDefault))
+ }
+ if Sizes[2].Limit != TierChildren || Sizes[2].Shares != SharesOf(TierChildren) {
+ t.Errorf("large = %+v, want TierChildren %+v with shares %+v", Sizes[2], TierChildren, SharesOf(TierChildren))
+ }
+
+ for _, w := range want {
+ got, ok := SizeByName(w.Name)
+ if !ok || got != w {
+ t.Errorf("SizeByName(%q) = %+v, %v; want %+v, true", w.Name, got, ok, w)
+ }
+ }
+ for _, name := range []string{"", "Large", "default", "medium"} {
+ if got, ok := SizeByName(name); ok {
+ t.Errorf("SizeByName(%q) = %+v, true; want not ok", name, got)
+ }
+ }
+
+ if got, ok := SizeOf(TierDefault); !ok || got.Name != "standard" {
+ t.Errorf("SizeOf(TierDefault) = %+v, %v; want standard", got, ok)
+ }
+ if got, ok := SizeOf(TierChildren); !ok || got.Name != "large" {
+ t.Errorf("SizeOf(TierChildren) = %+v, %v; want large", got, ok)
+ }
+ if got, ok := SizeOf(Limit{MemoryMax: 3 << 30}); ok {
+ t.Errorf("SizeOf(3G) = %+v, true; want not ok", got)
+ }
+
+ names := SizeNames()
+ if wantNames := []string{"small", "standard", "large", "xlarge"}; !reflect.DeepEqual(names, wantNames) {
+ t.Errorf("SizeNames() = %q, want %q", names, wantNames)
+ }
+ // A caller that edits the returned slice does not edit the table.
+ names[0] = "changed"
+ if Sizes[0].Name != "small" || SizeNames()[0] != "small" {
+ t.Errorf("SizeNames() shares storage with Sizes")
+ }
+}
+
+// TestSizesTableIsLiteral pins Astra plan R1-10: plan 3 derives TierDefault,
+// TierChildren and SharesOf from Sizes, so an initializer that named any of
+// them would then be an initialization cycle.
+func TestSizesTableIsLiteral(t *testing.T) {
+ f, err := parser.ParseFile(token.NewFileSet(), "sizes.go", nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ banned := map[string]bool{"TierDefault": true, "TierChildren": true, "SharesOf": true, "HighOf": true}
+ found := false
+ for _, decl := range f.Decls {
+ gen, ok := decl.(*ast.GenDecl)
+ if !ok || gen.Tok != token.VAR {
+ continue
+ }
+ for _, spec := range gen.Specs {
+ vs := spec.(*ast.ValueSpec)
+ for i, name := range vs.Names {
+ if name.Name != "Sizes" {
+ continue
+ }
+ if i >= len(vs.Values) {
+ t.Fatalf("Sizes has no initializer")
+ }
+ found = true
+ ast.Inspect(vs.Values[i], func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok && banned[id.Name] {
+ t.Errorf("Sizes initializer names %s; every row must be a literal", id.Name)
+ }
+ return true
+ })
+ }
+ }
+ }
+ if !found {
+ t.Fatalf("no `var Sizes = ...` in sizes.go")
+ }
+}