@@ -20,13 +20,16 @@ import (
20
20
"fmt"
21
21
"strconv"
22
22
23
+ "slices"
24
+
23
25
apiv1 "k8s.io/api/core/v1"
24
26
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
25
27
"k8s.io/autoscaler/cluster-autoscaler/metrics"
26
28
core_utils "k8s.io/autoscaler/cluster-autoscaler/simulator"
27
29
"k8s.io/autoscaler/cluster-autoscaler/simulator/clustersnapshot"
28
30
"k8s.io/autoscaler/cluster-autoscaler/simulator/framework"
29
31
"k8s.io/klog/v2"
32
+ "k8s.io/kubernetes/pkg/scheduler/framework/plugins/podtopologyspread"
30
33
)
31
34
32
35
// BinpackingNodeEstimator estimates the number of needed nodes to handle the given amount of pods.
@@ -171,7 +174,8 @@ func (e *BinpackingNodeEstimator) tryToScheduleOnNewNodes(
171
174
172
175
if estimationState .lastNodeName != "" {
173
176
// Try to schedule the pod on only newly created node.
174
- if err := e .clusterSnapshot .SchedulePod (pod , estimationState .lastNodeName ); err == nil {
177
+ err := e .clusterSnapshot .SchedulePod (pod , estimationState .lastNodeName )
178
+ if err == nil {
175
179
// The pod was scheduled on the newly created node.
176
180
found = true
177
181
estimationState .trackScheduledPod (pod , estimationState .lastNodeName )
@@ -180,6 +184,24 @@ func (e *BinpackingNodeEstimator) tryToScheduleOnNewNodes(
180
184
return false , err
181
185
}
182
186
// The pod can't be scheduled on the newly created node because of scheduling predicates.
187
+
188
+ // Check if node failed because of topology constraints.
189
+ if isPodUsingHostNameTopologyKey (pod ) && hasTopologyConstraintError (err ) {
190
+ // If the pod can't be scheduled on the last node because of topology constraints, we can stop binpacking.
191
+ // The pod can't be scheduled on any new node either, because it has the same topology constraints.
192
+ nodeName , err := e .clusterSnapshot .SchedulePodOnAnyNodeMatching (pod , func (nodeInfo * framework.NodeInfo ) bool {
193
+ return nodeInfo .Node ().Name != estimationState .lastNodeName // only skip the last node that failed scheduling
194
+ })
195
+ if err != nil && err .Type () == clustersnapshot .SchedulingInternalError {
196
+ // Unexpected error.
197
+ return false , err
198
+ }
199
+ if nodeName != "" {
200
+ // The pod was scheduled on a different node, so we can continue binpacking.
201
+ found = true
202
+ estimationState .trackScheduledPod (pod , nodeName )
203
+ }
204
+ }
183
205
}
184
206
185
207
if ! found {
@@ -240,6 +262,33 @@ func (e *BinpackingNodeEstimator) addNewNodeToSnapshot(
240
262
return nil
241
263
}
242
264
265
+ // isTopologyConstraintError determines if an error is related to pod topology spread constraints
266
+ // by checking the predicate name and reasons
267
+ func hasTopologyConstraintError (err clustersnapshot.SchedulingError ) bool {
268
+ if err == nil {
269
+ return false
270
+ }
271
+
272
+ // Check reasons for mentions of topology or constraints
273
+ return slices .Contains (err .FailingPredicateReasons (), podtopologyspread .ErrReasonConstraintsNotMatch )
274
+ }
275
+
276
+ // isPodUsingHostNameTopoKey returns true if the pod has any topology spread
277
+ // constraint that uses the kubernetes.io/hostname topology key
278
+ func isPodUsingHostNameTopologyKey (pod * apiv1.Pod ) bool {
279
+ if pod == nil || pod .Spec .TopologySpreadConstraints == nil {
280
+ return false
281
+ }
282
+
283
+ for _ , constraint := range pod .Spec .TopologySpreadConstraints {
284
+ if constraint .TopologyKey == apiv1 .LabelHostname {
285
+ return true
286
+ }
287
+ }
288
+
289
+ return false
290
+ }
291
+
243
292
func observeBinpackingHeterogeneity (podsEquivalenceGroups []PodEquivalenceGroup , nodeTemplate * framework.NodeInfo ) {
244
293
node := nodeTemplate .Node ()
245
294
var instanceType , cpuCount string
0 commit comments