@@ -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