Skip to content

Commit 88f09ba

Browse files
committed
feat(controller): effectiveMaxVMs + parseFraction capacity helpers
1 parent 0ec1c30 commit 88f09ba

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

internal/controller/capacity.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"strconv"
21+
)
22+
23+
// parseFraction parses a fraction string (e.g. "0.9") into a float64 in [0,1].
24+
// Returns 0.9 for empty, unparseable, or out-of-range input.
25+
func parseFraction(s string) float64 {
26+
if s == "" {
27+
return 0.9
28+
}
29+
v, err := strconv.ParseFloat(s, 64)
30+
if err != nil || v < 0 || v > 1 {
31+
return 0.9
32+
}
33+
return v
34+
}
35+
36+
// effectiveMaxVMs returns the maximum number of VMs that fit on a node given
37+
// the node's allocatable resources, the VM's compute requirements, and the
38+
// capacity fraction.
39+
//
40+
// effectiveMax = min(
41+
// floor(allocCPUMillicores * fraction / vmCPUMillicores),
42+
// floor(allocMemBytes * fraction / vmMemBytes),
43+
// )
44+
//
45+
// Returns 0 if either dimension cannot fit even one VM.
46+
func effectiveMaxVMs(allocCPUMillis, allocMemBytes int64, vcpu, memMiB int32, fraction float64) int32 {
47+
vmCPUMillis := int64(vcpu) * 1000
48+
cpuMax := int64(float64(allocCPUMillis)*fraction) / vmCPUMillis
49+
50+
vmMemBytes := int64(memMiB) * 1024 * 1024
51+
memMax := int64(float64(allocMemBytes)*fraction) / vmMemBytes
52+
53+
result := cpuMax
54+
if memMax < cpuMax {
55+
result = memMax
56+
}
57+
if result < 0 {
58+
return 0
59+
}
60+
return int32(result) //nolint:gosec
61+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestParseFraction(t *testing.T) {
24+
cases := []struct {
25+
in string
26+
want float64
27+
}{
28+
{"0.9", 0.9},
29+
{"1.0", 1.0},
30+
{"0", 0.0},
31+
{"0.5", 0.5},
32+
{"", 0.9}, // empty → default
33+
{"invalid", 0.9}, // bad string → default
34+
{"1.1", 0.9}, // out of range → default
35+
{"-0.1", 0.9}, // negative → default
36+
}
37+
for _, tc := range cases {
38+
got := parseFraction(tc.in)
39+
if got != tc.want {
40+
t.Errorf("parseFraction(%q) = %v, want %v", tc.in, got, tc.want)
41+
}
42+
}
43+
}
44+
45+
func TestEffectiveMaxVMs(t *testing.T) {
46+
cases := []struct {
47+
name string
48+
allocCPUm int64 // node allocatable CPU in millicores
49+
allocMemBytes int64 // node allocatable memory in bytes
50+
vcpu int32 // VM vCPUs
51+
memMiB int32 // VM memory in MiB
52+
fraction float64
53+
want int32
54+
}{
55+
{
56+
name: "cpu-bound: 4 CPUs, 0.9 fraction, 1 vcpu VMs",
57+
// 4000m * 0.9 / 1000m = 3.6 → floor 3
58+
allocCPUm: 4000, allocMemBytes: 64 * 1024 * 1024 * 1024,
59+
vcpu: 1, memMiB: 256, fraction: 0.9,
60+
want: 3,
61+
},
62+
{
63+
name: "memory-bound: 1GiB, 0.9 fraction, 512MiB VMs",
64+
// cpu: 16000m * 0.9 / 1000m = 14; mem: 1GiB * 0.9 / 512MiB = 1.8 → floor 1
65+
allocCPUm: 16000, allocMemBytes: 1 * 1024 * 1024 * 1024,
66+
vcpu: 1, memMiB: 512, fraction: 0.9,
67+
want: 1,
68+
},
69+
{
70+
name: "both equal: 4 VMs fit by both CPU and memory",
71+
// cpu: 4000m * 1.0 / 1000m = 4; mem: 4*512MiB * 1.0 / 512MiB = 4
72+
allocCPUm: 4000, allocMemBytes: 4 * 512 * 1024 * 1024,
73+
vcpu: 1, memMiB: 512, fraction: 1.0,
74+
want: 4,
75+
},
76+
{
77+
name: "fraction zero → 0 VMs",
78+
allocCPUm: 8000, allocMemBytes: 16 * 1024 * 1024 * 1024,
79+
vcpu: 1, memMiB: 512, fraction: 0.0,
80+
want: 0,
81+
},
82+
{
83+
name: "VM larger than node → 0",
84+
// node has 2 CPUs, VM wants 4 → 0
85+
allocCPUm: 2000, allocMemBytes: 64 * 1024 * 1024 * 1024,
86+
vcpu: 4, memMiB: 256, fraction: 0.9,
87+
want: 0,
88+
},
89+
{
90+
name: "8 CPUs 0.9 fraction 2vcpu VMs",
91+
// 8000m * 0.9 / 2000m = 3.6 → floor 3; mem plenty
92+
allocCPUm: 8000, allocMemBytes: 64 * 1024 * 1024 * 1024,
93+
vcpu: 2, memMiB: 512, fraction: 0.9,
94+
want: 3,
95+
},
96+
}
97+
for _, tc := range cases {
98+
t.Run(tc.name, func(t *testing.T) {
99+
got := effectiveMaxVMs(tc.allocCPUm, tc.allocMemBytes, tc.vcpu, tc.memMiB, tc.fraction)
100+
if got != tc.want {
101+
t.Errorf("effectiveMaxVMs = %d, want %d", got, tc.want)
102+
}
103+
})
104+
}
105+
}

0 commit comments

Comments
 (0)