Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions internal/service/scheduler/vmscheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ func selectNode(
sort.Sort(byReplicas)

decision := byMemory[0].Name
if requestedMemory < byReplicas[0].AvailableMemory {
for _, info := range byReplicas {
// distribute round-robin when memory allows it
decision = byReplicas[0].Name
if requestedMemory < info.AvailableMemory {
decision = info.Name
break
}
}

if logger := logr.FromContextOrDiscard(ctx); logger.V(4).Enabled() {
Expand Down
63 changes: 63 additions & 0 deletions internal/service/scheduler/vmscheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,69 @@ func TestSelectNode(t *testing.T) {
})
}

func TestSelectNodeEvenlySpread(t *testing.T) {
// Verify that VMs are scheduled evenly across nodes when memory allows
allowedNodes := []string{"pve1", "pve2", "pve3"}
var locations []infrav1.NodeLocation
const requestMiB = 8
availableMem := map[string]uint64{
"pve1": miBytes(25), // enough for 3 VMs
"pve2": miBytes(35), // enough for 4 VMs
"pve3": miBytes(15), // enough for 1 VM
}

expectedNodes := []string{
// initial round-robin: everyone has enough memory
"pve2", "pve1", "pve3",
// second round-robin: pve3 out of memory
"pve2", "pve1", "pve2",
// third round-robin: pve1 and pve2 has room for one more VM each
"pve1", "pve2",
}

for i, expectedNode := range expectedNodes {
t.Run(fmt.Sprintf("round %d", i+1), func(t *testing.T) {
proxmoxMachine := &infrav1.ProxmoxMachine{
Spec: infrav1.ProxmoxMachineSpec{
MemoryMiB: requestMiB,
},
}

client := fakeResourceClient(availableMem)

node, err := selectNode(context.Background(), client, proxmoxMachine, locations, allowedNodes, &infrav1.SchedulerHints{})
require.NoError(t, err)
require.Equal(t, expectedNode, node)

require.Greater(t, availableMem[node], miBytes(requestMiB))
availableMem[node] -= miBytes(requestMiB)

locations = append(locations, infrav1.NodeLocation{Node: node})
})
}

t.Run("out of memory", func(t *testing.T) {
proxmoxMachine := &infrav1.ProxmoxMachine{
Spec: infrav1.ProxmoxMachineSpec{
MemoryMiB: requestMiB,
},
}

client := fakeResourceClient(availableMem)

node, err := selectNode(context.Background(), client, proxmoxMachine, locations, allowedNodes, &infrav1.SchedulerHints{})
require.ErrorAs(t, err, &InsufficientMemoryError{})
require.Empty(t, node)

expectMem := map[string]uint64{
"pve1": miBytes(1), // 25 - 8 x 3
"pve2": miBytes(3), // 35 - 8 x 4
"pve3": miBytes(7), // 15 - 8 x 1
}
require.Equal(t, expectMem, availableMem)
})
}

func TestScheduleVM(t *testing.T) {
ctrlClient := setupClient()
require.NotNil(t, ctrlClient)
Expand Down
Loading