@@ -107,67 +107,79 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa
107107 return nil , err
108108 }
109109 for _ , reservation := range reservations .Items {
110- if ! reservation .IsReady () {
111- continue // Only consider active reservations (Ready=True).
112- }
113-
114- // Check if this reservation type should be ignored
110+ // Check if this reservation type should be ignored — applies regardless of ready state.
115111 if slices .Contains (s .Options .IgnoredReservationTypes , reservation .Spec .Type ) {
116112 traceLog .Debug ("ignoring reservation type" , "type" , reservation .Spec .Type , "reservation" , reservation .Name )
117113 continue
118114 }
119115
120- // Handle reservation based on its type.
121- switch reservation .Spec .Type {
122- case v1alpha1 .ReservationTypeCommittedResource , "" : // Empty string for backward compatibility
123- // Skip if no CommittedResourceReservation spec or no resource group set.
124- if reservation .Spec .CommittedResourceReservation == nil || reservation .Spec .CommittedResourceReservation .ResourceGroup == "" {
125- continue // Not handled by us (no resource group set).
116+ if ! reservation .IsReady () {
117+ if reservation .Spec .TargetHost == "" && reservation .Status .Host == "" {
118+ continue // not placed yet, nothing to block
126119 }
120+ // TargetHost is set but Ready has not been synced yet: the reservation controller
121+ // wrote the host in reconcile cycle 1 but hasn't completed cycle 2 (status sync).
122+ // Block the full slot now to prevent a concurrent scheduling call from picking the
123+ // same host. Unlock logic is intentionally skipped — an unconfirmed slot must not
124+ // be unlocked for any VM or project.
125+ traceLog .Warn ("reservation has target host set but is not yet ready — blocking host to prevent double-booking" ,
126+ "reservation" , reservation .Name ,
127+ "targetHost" , reservation .Spec .TargetHost ,
128+ "statusHost" , reservation .Status .Host ,
129+ )
130+ } else {
131+ // Ready reservation: apply type-specific unlock logic.
132+ switch reservation .Spec .Type {
133+ case v1alpha1 .ReservationTypeCommittedResource , "" : // Empty string for backward compatibility
134+ // Skip if no CommittedResourceReservation spec or no resource group set.
135+ if reservation .Spec .CommittedResourceReservation == nil || reservation .Spec .CommittedResourceReservation .ResourceGroup == "" {
136+ continue // Not handled by us (no resource group set).
137+ }
127138
128- // Check if this is a CR reservation scheduling request.
129- // If so, we should NOT unlock any CR reservations to prevent overbooking.
130- // CR capacity should only be unlocked for actual VM scheduling.
131- intent , err := request .GetIntent ()
132- switch {
133- case err == nil && intent == api .ReserveForCommittedResourceIntent :
134- traceLog .Debug ("keeping CR reservation locked for CR reservation scheduling" ,
135- "reservation" , reservation .Name ,
136- "intent" , intent )
137- // Don't continue - fall through to block the resources
138- case ! s .Options .LockReserved &&
139- // For committed resource reservations: unlock resources only if:
140- // 1. Project ID matches
141- // 2. ResourceGroup matches the flavor's hw_version
142- reservation .Spec .CommittedResourceReservation .ProjectID == request .Spec .Data .ProjectID &&
143- reservation .Spec .CommittedResourceReservation .ResourceGroup == request .Spec .Data .Flavor .Data .ExtraSpecs ["hw_version" ]:
144- traceLog .Info ("unlocking resources reserved by matching committed resource reservation with allocation" ,
145- "reservation" , reservation .Name ,
146- "instanceUUID" , request .Spec .Data .InstanceUUID ,
147- "projectID" , request .Spec .Data .ProjectID ,
148- "resourceGroup" , reservation .Spec .CommittedResourceReservation .ResourceGroup )
149- continue
150- }
139+ // Check if this is a CR reservation scheduling request.
140+ // If so, we should NOT unlock any CR reservations to prevent overbooking.
141+ // CR capacity should only be unlocked for actual VM scheduling.
142+ intent , err := request .GetIntent ()
143+ switch {
144+ case err == nil && intent == api .ReserveForCommittedResourceIntent :
145+ traceLog .Debug ("keeping CR reservation locked for CR reservation scheduling" ,
146+ "reservation" , reservation .Name ,
147+ "intent" , intent )
148+ // Don't continue - fall through to block the resources
149+ case ! s .Options .LockReserved &&
150+ // For committed resource reservations: unlock resources only if:
151+ // 1. Project ID matches
152+ // 2. ResourceGroup matches the flavor's hw_version
153+ reservation .Spec .CommittedResourceReservation .ProjectID == request .Spec .Data .ProjectID &&
154+ reservation .Spec .CommittedResourceReservation .ResourceGroup == request .Spec .Data .Flavor .Data .ExtraSpecs ["hw_version" ]:
155+ traceLog .Info ("unlocking resources reserved by matching committed resource reservation with allocation" ,
156+ "reservation" , reservation .Name ,
157+ "instanceUUID" , request .Spec .Data .InstanceUUID ,
158+ "projectID" , request .Spec .Data .ProjectID ,
159+ "resourceGroup" , reservation .Spec .CommittedResourceReservation .ResourceGroup )
160+ continue
161+ }
151162
152- case v1alpha1 .ReservationTypeFailover :
153- // For failover reservations: if the requested VM is contained in the allocations map
154- // AND this is an evacuation request, unlock the resources.
155- // We only unlock during evacuations because:
156- // 1. Failover reservations are specifically for HA/evacuation scenarios.
157- // 2. During live migrations or other operations, we don't want to use failover capacity.
158- // Note: we cannot use failover reservations from other VMs, as that can invalidate our HA guarantees.
159- intent , err := request .GetIntent ()
160- if err == nil && intent == api .EvacuateIntent {
161- if reservation .Status .FailoverReservation != nil {
162- if _ , contained := reservation .Status .FailoverReservation .Allocations [request .Spec .Data .InstanceUUID ]; contained {
163- traceLog .Info ("unlocking resources reserved by failover reservation for VM in allocations (evacuation)" ,
164- "reservation" , reservation .Name ,
165- "instanceUUID" , request .Spec .Data .InstanceUUID )
166- continue
163+ case v1alpha1 .ReservationTypeFailover :
164+ // For failover reservations: if the requested VM is contained in the allocations map
165+ // AND this is an evacuation request, unlock the resources.
166+ // We only unlock during evacuations because:
167+ // 1. Failover reservations are specifically for HA/evacuation scenarios.
168+ // 2. During live migrations or other operations, we don't want to use failover capacity.
169+ // Note: we cannot use failover reservations from other VMs, as that can invalidate our HA guarantees.
170+ intent , err := request .GetIntent ()
171+ if err == nil && intent == api .EvacuateIntent {
172+ if reservation .Status .FailoverReservation != nil {
173+ if _ , contained := reservation .Status .FailoverReservation .Allocations [request .Spec .Data .InstanceUUID ]; contained {
174+ traceLog .Info ("unlocking resources reserved by failover reservation for VM in allocations (evacuation)" ,
175+ "reservation" , reservation .Name ,
176+ "instanceUUID" , request .Spec .Data .InstanceUUID )
177+ continue
178+ }
167179 }
168180 }
181+ traceLog .Debug ("processing failover reservation" , "reservation" , reservation .Name )
169182 }
170- traceLog .Debug ("processing failover reservation" , "reservation" , reservation .Name )
171183 }
172184
173185 // Block resources on BOTH Spec.TargetHost (desired) AND Status.Host (actual).
0 commit comments