Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ type FilterHasEnoughCapacity struct {
//
// In case the project and flavor match, space reserved is unlocked (slotting).
//
// Capacity accounting uses two sources: hv.Status.Allocation (aggregate real-time usage of
// all running VMs) and Reservation.Status.Allocations (which VMs are confirmed on a slot,
// maintained by the reservation controller with a one-reconcile-cycle lag). During the window
// between a VM starting and the reservation controller reconciling, a VM appears in both
// sources — a conservative transient over-count that self-corrects on the next reconcile.
//
// During a CR reservation migration (TargetHost != Status.Host), both the source and target
// host are blocked with the full slot. The source block is intentionally conservative to
// preserve rollback capacity if the migration fails.
//
// Please note that, if num_instances is larger than 1, there needs to be enough
// capacity to place all instances on the same host. This limitation is necessary
// because we can't spread out instances, as the final set of valid hosts is not
Expand Down Expand Up @@ -170,41 +180,61 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
continue
}

// For CR reservations with allocations, calculate remaining (unallocated) resources to block.
// This prevents double-blocking of resources already consumed by running instances.
// For CR reservations with allocations, compute the effective block:
// confirmed = sum of resources for VMs present in both Spec and Status allocations
// specOnly = sum of resources for VMs present in Spec but not yet in Status
// remaining = max(0, Spec.Resources - confirmed) [clamped: never negative]
// block = max(remaining, specOnly) [spec-only VM must be fully covered]
//
// Clamping: if confirmed VMs exceed slot size (e.g. after resize), block = 0.
// Oversize spec-only: if a pending VM is larger than the remaining slot, block its full size.
var resourcesToBlock map[hv1.ResourceName]resource.Quantity
if reservation.Spec.Type == v1alpha1.ReservationTypeCommittedResource &&
// if the reservation is not being migrated, block only unused resources
reservation.Spec.TargetHost == reservation.Status.Host &&
reservation.Spec.CommittedResourceReservation != nil &&
reservation.Status.CommittedResourceReservation != nil &&
len(reservation.Spec.CommittedResourceReservation.Allocations) > 0 &&
len(reservation.Status.CommittedResourceReservation.Allocations) > 0 {
// Start with full reservation resources
resourcesToBlock = make(map[hv1.ResourceName]resource.Quantity)
for k, v := range reservation.Spec.Resources {
resourcesToBlock[k] = v.DeepCopy()
len(reservation.Spec.CommittedResourceReservation.Allocations) > 0 {
confirmedResources := make(map[hv1.ResourceName]resource.Quantity)
specOnlyResources := make(map[hv1.ResourceName]resource.Quantity)

statusAllocs := map[string]string{}
if reservation.Status.CommittedResourceReservation != nil {
statusAllocs = reservation.Status.CommittedResourceReservation.Allocations
}

// Subtract already-allocated resources because those consume already resources on the host
for instanceUUID, allocation := range reservation.Spec.CommittedResourceReservation.Allocations {
// Only subtract if allocation is already present in status (VM is actually running)
if _, isRunning := reservation.Status.CommittedResourceReservation.Allocations[instanceUUID]; !isRunning {
continue
}

_, isConfirmed := statusAllocs[instanceUUID]
for resourceName, quantity := range allocation.Resources {
if current, ok := resourcesToBlock[resourceName]; ok {
current.Sub(quantity)
resourcesToBlock[resourceName] = current
traceLog.Debug("subtracting allocated resources from reservation",
"reservation", reservation.Name,
"instanceUUID", instanceUUID,
"resource", resourceName,
"quantity", quantity.String())
if isConfirmed {
existing := confirmedResources[resourceName]
existing.Add(quantity)
confirmedResources[resourceName] = existing
} else {
existing := specOnlyResources[resourceName]
existing.Add(quantity)
specOnlyResources[resourceName] = existing
}
}
}

resourcesToBlock = make(map[hv1.ResourceName]resource.Quantity)
zero := resource.Quantity{}
for resourceName, slotSize := range reservation.Spec.Resources {
confirmed := confirmedResources[resourceName]
specOnly := specOnlyResources[resourceName]

remaining := slotSize.DeepCopy()
remaining.Sub(confirmed)
if remaining.Cmp(zero) < 0 {
remaining = zero.DeepCopy()
}

if specOnly.Cmp(remaining) > 0 {
resourcesToBlock[resourceName] = specOnly.DeepCopy()
} else {
resourcesToBlock[resourceName] = remaining
}
}
} else {
// For other reservation types or CR without allocations, block full resources
resourcesToBlock = reservation.Spec.Resources
Expand All @@ -229,7 +259,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
"reservationType", reservation.Spec.Type,
"freeCPU", freeCPU.String(),
"blocked", cpu.String())
freeCPU = resource.MustParse("0")
freeCPU = resource.Quantity{}
}
freeResourcesByHost[host]["cpu"] = freeCPU
}
Expand All @@ -244,7 +274,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
"reservationType", reservation.Spec.Type,
"freeMemory", freeMemory.String(),
"blocked", memory.String())
freeMemory = resource.MustParse("0")
freeMemory = resource.Quantity{}
}
freeResourcesByHost[host]["memory"] = freeMemory
}
Expand Down
Loading
Loading