Skip to content

Commit 1401e19

Browse files
authored
feat: Add fallback to capacity filter and external customer filter (#593)
1 parent b3401a2 commit 1401e19

4 files changed

Lines changed: 143 additions & 13 deletions

File tree

internal/scheduling/nova/plugins/filters/filter_external_customer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ func (s *FilterExternalCustomerStep) Run(traceLog *slog.Logger, request api.Exte
3737
result := s.IncludeAllHostsFromRequest(request)
3838
domainName, err := request.Spec.Data.GetSchedulerHintStr("domain_name")
3939
if err != nil {
40-
traceLog.Error("failed to get domain_name scheduler hint", "error", err)
41-
return nil, err
40+
traceLog.Error("failed to get domain_name scheduler hint, skipping filter", "error", err)
41+
return result, nil
4242
}
4343
if slices.Contains(s.Options.CustomerIgnoredDomainNames, domainName) {
4444
traceLog.Info("domain is no external customer domain, skipping filter", "domain", domainName)

internal/scheduling/nova/plugins/filters/filter_external_customer_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func TestFilterExternalCustomerStep_Run(t *testing.T) {
245245
filteredHosts: []string{"host3"},
246246
},
247247
{
248-
name: "Missing domain_name in scheduler hints - error",
248+
name: "Missing domain_name in scheduler hints - skips filter, all hosts pass",
249249
opts: FilterExternalCustomerStepOpts{
250250
CustomerDomainNamePrefixes: []string{"ext-"},
251251
},
@@ -257,12 +257,14 @@ func TestFilterExternalCustomerStep_Run(t *testing.T) {
257257
},
258258
Hosts: []api.ExternalSchedulerHost{
259259
{ComputeHost: "host1"},
260+
{ComputeHost: "host3"},
260261
},
261262
},
262-
expectError: true,
263+
expectedHosts: []string{"host1", "host3"},
264+
filteredHosts: []string{},
263265
},
264266
{
265-
name: "Nil scheduler hints - error",
267+
name: "Nil scheduler hints - skips filter, all hosts pass",
266268
opts: FilterExternalCustomerStepOpts{
267269
CustomerDomainNamePrefixes: []string{"ext-"},
268270
},
@@ -274,9 +276,11 @@ func TestFilterExternalCustomerStep_Run(t *testing.T) {
274276
},
275277
Hosts: []api.ExternalSchedulerHost{
276278
{ComputeHost: "host1"},
279+
{ComputeHost: "host2"},
277280
},
278281
},
279-
expectError: true,
282+
expectedHosts: []string{"host1", "host2"},
283+
filteredHosts: []string{},
280284
},
281285
{
282286
name: "Case sensitive prefix matching",

internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,14 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
5656
return nil, err
5757
}
5858
for _, hv := range hvs.Items {
59-
// This case would be caught below, but we want to log this explicitly.
6059
if hv.Status.EffectiveCapacity == nil {
61-
traceLog.Warn("hypervisor with nil effective capacity, skipping", "host", hv.Name)
62-
continue
60+
traceLog.Warn("hypervisor with nil effective capacity, use capacity instead (overprovisioning not considered)", "host", hv.Name)
61+
freeResourcesByHost[hv.Name] = hv.Status.Capacity
62+
} else {
63+
// Start with the total effective capacity which is capacity * overcommit ratio.
64+
freeResourcesByHost[hv.Name] = hv.Status.EffectiveCapacity
6365
}
6466

65-
// Start with the total effective capacity which is capacity * overcommit ratio.
66-
freeResourcesByHost[hv.Name] = hv.Status.EffectiveCapacity
67-
6867
// Subtract allocated resources.
6968
for resourceName, allocated := range hv.Status.Allocation {
7069
free, ok := freeResourcesByHost[hv.Name][resourceName]

internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,49 @@ func newHypervisor(name, cpuCap, cpuAlloc, memCap, memAlloc string) *hv1.Hypervi
5151
}
5252
}
5353

54+
// newHypervisorWithCapacityOnly creates a hypervisor with only Capacity set (no EffectiveCapacity).
55+
func newHypervisorWithCapacityOnly(name, cpuCap, memCap string) *hv1.Hypervisor {
56+
return &hv1.Hypervisor{
57+
ObjectMeta: metav1.ObjectMeta{
58+
Name: name,
59+
},
60+
Status: hv1.HypervisorStatus{
61+
Capacity: map[hv1.ResourceName]resource.Quantity{
62+
hv1.ResourceCPU: resource.MustParse(cpuCap),
63+
hv1.ResourceMemory: resource.MustParse(memCap),
64+
},
65+
Allocation: map[hv1.ResourceName]resource.Quantity{
66+
hv1.ResourceCPU: resource.MustParse("0"),
67+
hv1.ResourceMemory: resource.MustParse("0"),
68+
},
69+
},
70+
}
71+
}
72+
73+
// newHypervisorWithBothCapacities creates a hypervisor with both Capacity and EffectiveCapacity set.
74+
// EffectiveCapacity is typically >= Capacity due to overcommit ratio.
75+
func newHypervisorWithBothCapacities(name, cpuCap, cpuEffCap, memCap, memEffCap string) *hv1.Hypervisor {
76+
return &hv1.Hypervisor{
77+
ObjectMeta: metav1.ObjectMeta{
78+
Name: name,
79+
},
80+
Status: hv1.HypervisorStatus{
81+
Capacity: map[hv1.ResourceName]resource.Quantity{
82+
hv1.ResourceCPU: resource.MustParse(cpuCap),
83+
hv1.ResourceMemory: resource.MustParse(memCap),
84+
},
85+
EffectiveCapacity: map[hv1.ResourceName]resource.Quantity{
86+
hv1.ResourceCPU: resource.MustParse(cpuEffCap),
87+
hv1.ResourceMemory: resource.MustParse(memEffCap),
88+
},
89+
Allocation: map[hv1.ResourceName]resource.Quantity{
90+
hv1.ResourceCPU: resource.MustParse("0"),
91+
hv1.ResourceMemory: resource.MustParse("0"),
92+
},
93+
},
94+
}
95+
}
96+
5497
func newCommittedReservation(
5598
name, targetHost, observedHost, projectID, flavorName, flavorGroup, cpu, memory string,
5699
specAllocations map[string]v1alpha1.CommittedResourceAllocation, // Spec allocations for CR
@@ -440,7 +483,7 @@ func TestFilterHasEnoughCapacity_ReservationTypes(t *testing.T) {
440483
t.Run(tt.name, func(t *testing.T) {
441484
objects := make([]client.Object, 0, len(hypervisors)+len(tt.reservations))
442485
for _, h := range hypervisors {
443-
objects = append(objects, h)
486+
objects = append(objects, h.DeepCopy())
444487
}
445488
for _, r := range tt.reservations {
446489
objects = append(objects, r)
@@ -469,3 +512,87 @@ func TestFilterHasEnoughCapacity_ReservationTypes(t *testing.T) {
469512
})
470513
}
471514
}
515+
516+
func TestFilterHasEnoughCapacity_NilEffectiveCapacityFallback(t *testing.T) {
517+
scheme := buildTestScheme(t)
518+
519+
tests := []struct {
520+
name string
521+
hypervisors []*hv1.Hypervisor
522+
request api.ExternalSchedulerRequest
523+
expectedHosts []string
524+
filteredHosts []string
525+
}{
526+
{
527+
name: "Hypervisor with nil EffectiveCapacity uses Capacity fallback",
528+
hypervisors: []*hv1.Hypervisor{
529+
newHypervisor("host1", "16", "8", "32Gi", "16Gi"), // has EffectiveCapacity: 8 CPU free, 16Gi free
530+
newHypervisorWithCapacityOnly("host2", "8", "16Gi"), // nil EffectiveCapacity, uses Capacity: 8 CPU free, 16Gi free
531+
newHypervisorWithCapacityOnly("host3", "2", "4Gi"), // nil EffectiveCapacity, uses Capacity: 2 CPU free (not enough)
532+
newHypervisorWithCapacityOnly("host4", "16", "32Gi"), // nil EffectiveCapacity, uses Capacity: 16 CPU free, 32Gi free
533+
},
534+
request: newNovaRequest("instance-123", "project-A", "m1.small", "gp-1", 4, "8Gi", false, []string{"host1", "host2", "host3", "host4"}),
535+
expectedHosts: []string{"host1", "host2", "host4"},
536+
filteredHosts: []string{"host3"},
537+
},
538+
{
539+
name: "All hypervisors with nil EffectiveCapacity use Capacity fallback",
540+
hypervisors: []*hv1.Hypervisor{
541+
newHypervisorWithCapacityOnly("host1", "8", "16Gi"),
542+
newHypervisorWithCapacityOnly("host2", "4", "8Gi"),
543+
},
544+
request: newNovaRequest("instance-123", "project-A", "m1.small", "gp-1", 4, "8Gi", false, []string{"host1", "host2"}),
545+
expectedHosts: []string{"host1", "host2"},
546+
filteredHosts: []string{},
547+
},
548+
{
549+
name: "EffectiveCapacity used when both are set (overcommit scenario)",
550+
hypervisors: []*hv1.Hypervisor{
551+
// host1: Capacity=8 CPU, EffectiveCapacity=16 CPU (2x overcommit)
552+
// With Capacity only: 8 free -> passes
553+
// With EffectiveCapacity: 16 free -> passes (more capacity available)
554+
newHypervisorWithBothCapacities("host1", "8", "16", "16Gi", "32Gi"),
555+
// host2: Capacity=4 CPU, EffectiveCapacity=8 CPU (2x overcommit)
556+
// With Capacity only: 4 free -> would be filtered (need 5)
557+
// With EffectiveCapacity: 8 free -> passes
558+
newHypervisorWithBothCapacities("host2", "4", "8", "8Gi", "16Gi"),
559+
// host3: Capacity=4 CPU, EffectiveCapacity=4 CPU (no overcommit)
560+
// Both: 4 free -> filtered (need 5)
561+
newHypervisorWithBothCapacities("host3", "4", "4", "8Gi", "8Gi"),
562+
},
563+
request: newNovaRequest("instance-123", "project-A", "m1.small", "gp-1", 5, "8Gi", false, []string{"host1", "host2", "host3"}),
564+
expectedHosts: []string{"host1", "host2"},
565+
filteredHosts: []string{"host3"},
566+
},
567+
}
568+
569+
for _, tt := range tests {
570+
t.Run(tt.name, func(t *testing.T) {
571+
objects := make([]client.Object, 0, len(tt.hypervisors))
572+
for _, h := range tt.hypervisors {
573+
objects = append(objects, h.DeepCopy())
574+
}
575+
576+
step := &FilterHasEnoughCapacity{}
577+
step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
578+
step.Options = FilterHasEnoughCapacityOpts{LockReserved: false}
579+
580+
result, err := step.Run(slog.Default(), tt.request)
581+
if err != nil {
582+
t.Fatalf("expected no error, got %v", err)
583+
}
584+
585+
for _, host := range tt.expectedHosts {
586+
if _, ok := result.Activations[host]; !ok {
587+
t.Errorf("expected host %s to be present in activations, but got %+v", host, result.Activations)
588+
}
589+
}
590+
591+
for _, host := range tt.filteredHosts {
592+
if _, ok := result.Activations[host]; ok {
593+
t.Errorf("expected host %s to be filtered out", host)
594+
}
595+
}
596+
})
597+
}
598+
}

0 commit comments

Comments
 (0)