Skip to content

Commit e6ae001

Browse files
authored
fix: capacity filter correctly accounts for multi-VM CR reservation slots (#784)
Fixes capacity blocking for CommittedResource reservation slots that contain multiple VMs at different confirmation stages.
1 parent 8e9faa9 commit e6ae001

2 files changed

Lines changed: 488 additions & 236 deletions

File tree

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

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ type FilterHasEnoughCapacity struct {
4141
//
4242
// In case the project and flavor match, space reserved is unlocked (slotting).
4343
//
44+
// Capacity accounting uses two sources: hv.Status.Allocation (aggregate real-time usage of
45+
// all running VMs) and Reservation.Status.Allocations (which VMs are confirmed on a slot,
46+
// maintained by the reservation controller with a one-reconcile-cycle lag). During the window
47+
// between a VM starting and the reservation controller reconciling, a VM appears in both
48+
// sources — a conservative transient over-count that self-corrects on the next reconcile.
49+
//
50+
// During a CR reservation migration (TargetHost != Status.Host), both the source and target
51+
// host are blocked with the full slot. The source block is intentionally conservative to
52+
// preserve rollback capacity if the migration fails.
53+
//
4454
// Please note that, if num_instances is larger than 1, there needs to be enough
4555
// capacity to place all instances on the same host. This limitation is necessary
4656
// because we can't spread out instances, as the final set of valid hosts is not
@@ -170,41 +180,61 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
170180
continue
171181
}
172182

173-
// For CR reservations with allocations, calculate remaining (unallocated) resources to block.
174-
// This prevents double-blocking of resources already consumed by running instances.
183+
// For CR reservations with allocations, compute the effective block:
184+
// confirmed = sum of resources for VMs present in both Spec and Status allocations
185+
// specOnly = sum of resources for VMs present in Spec but not yet in Status
186+
// remaining = max(0, Spec.Resources - confirmed) [clamped: never negative]
187+
// block = max(remaining, specOnly) [spec-only VM must be fully covered]
188+
//
189+
// Clamping: if confirmed VMs exceed slot size (e.g. after resize), block = 0.
190+
// Oversize spec-only: if a pending VM is larger than the remaining slot, block its full size.
175191
var resourcesToBlock map[hv1.ResourceName]resource.Quantity
176192
if reservation.Spec.Type == v1alpha1.ReservationTypeCommittedResource &&
177193
// if the reservation is not being migrated, block only unused resources
178194
reservation.Spec.TargetHost == reservation.Status.Host &&
179195
reservation.Spec.CommittedResourceReservation != nil &&
180-
reservation.Status.CommittedResourceReservation != nil &&
181-
len(reservation.Spec.CommittedResourceReservation.Allocations) > 0 &&
182-
len(reservation.Status.CommittedResourceReservation.Allocations) > 0 {
183-
// Start with full reservation resources
184-
resourcesToBlock = make(map[hv1.ResourceName]resource.Quantity)
185-
for k, v := range reservation.Spec.Resources {
186-
resourcesToBlock[k] = v.DeepCopy()
196+
len(reservation.Spec.CommittedResourceReservation.Allocations) > 0 {
197+
confirmedResources := make(map[hv1.ResourceName]resource.Quantity)
198+
specOnlyResources := make(map[hv1.ResourceName]resource.Quantity)
199+
200+
statusAllocs := map[string]string{}
201+
if reservation.Status.CommittedResourceReservation != nil {
202+
statusAllocs = reservation.Status.CommittedResourceReservation.Allocations
187203
}
188204

189-
// Subtract already-allocated resources because those consume already resources on the host
190205
for instanceUUID, allocation := range reservation.Spec.CommittedResourceReservation.Allocations {
191-
// Only subtract if allocation is already present in status (VM is actually running)
192-
if _, isRunning := reservation.Status.CommittedResourceReservation.Allocations[instanceUUID]; !isRunning {
193-
continue
194-
}
195-
206+
_, isConfirmed := statusAllocs[instanceUUID]
196207
for resourceName, quantity := range allocation.Resources {
197-
if current, ok := resourcesToBlock[resourceName]; ok {
198-
current.Sub(quantity)
199-
resourcesToBlock[resourceName] = current
200-
traceLog.Debug("subtracting allocated resources from reservation",
201-
"reservation", reservation.Name,
202-
"instanceUUID", instanceUUID,
203-
"resource", resourceName,
204-
"quantity", quantity.String())
208+
if isConfirmed {
209+
existing := confirmedResources[resourceName]
210+
existing.Add(quantity)
211+
confirmedResources[resourceName] = existing
212+
} else {
213+
existing := specOnlyResources[resourceName]
214+
existing.Add(quantity)
215+
specOnlyResources[resourceName] = existing
205216
}
206217
}
207218
}
219+
220+
resourcesToBlock = make(map[hv1.ResourceName]resource.Quantity)
221+
zero := resource.Quantity{}
222+
for resourceName, slotSize := range reservation.Spec.Resources {
223+
confirmed := confirmedResources[resourceName]
224+
specOnly := specOnlyResources[resourceName]
225+
226+
remaining := slotSize.DeepCopy()
227+
remaining.Sub(confirmed)
228+
if remaining.Cmp(zero) < 0 {
229+
remaining = zero.DeepCopy()
230+
}
231+
232+
if specOnly.Cmp(remaining) > 0 {
233+
resourcesToBlock[resourceName] = specOnly.DeepCopy()
234+
} else {
235+
resourcesToBlock[resourceName] = remaining
236+
}
237+
}
208238
} else {
209239
// For other reservation types or CR without allocations, block full resources
210240
resourcesToBlock = reservation.Spec.Resources
@@ -229,7 +259,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
229259
"reservationType", reservation.Spec.Type,
230260
"freeCPU", freeCPU.String(),
231261
"blocked", cpu.String())
232-
freeCPU = resource.MustParse("0")
262+
freeCPU = resource.Quantity{}
233263
}
234264
freeResourcesByHost[host]["cpu"] = freeCPU
235265
}
@@ -244,7 +274,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
244274
"reservationType", reservation.Spec.Type,
245275
"freeMemory", freeMemory.String(),
246276
"blocked", memory.String())
247-
freeMemory = resource.MustParse("0")
277+
freeMemory = resource.Quantity{}
248278
}
249279
freeResourcesByHost[host]["memory"] = freeMemory
250280
}

0 commit comments

Comments
 (0)