Skip to content

Commit d7c325a

Browse files
committed
Enforce provisioning requests processing even if all pods are new
1 parent a587c55 commit d7c325a

File tree

9 files changed

+191
-3
lines changed

9 files changed

+191
-3
lines changed

cluster-autoscaler/core/static_autoscaler.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,15 @@ func (a *StaticAutoscaler) RunOnce(currentTime time.Time) caerrors.AutoscalerErr
520520
return false, nil
521521
}
522522

523+
forceScaleUp := a.processors.ScaleUpEnforcer.ShouldForceScaleUp(unschedulablePodsToHelp)
524+
523525
if len(unschedulablePodsToHelp) == 0 {
524526
scaleUpStatus.Result = status.ScaleUpNotNeeded
525527
klog.V(1).Info("No unschedulable pods")
526-
} else if a.MaxNodesTotal > 0 && len(readyNodes) >= a.MaxNodesTotal {
528+
} else if a.MaxNodesTotal > 0 && len(readyNodes) >= a.MaxNodesTotal && !forceScaleUp {
527529
scaleUpStatus.Result = status.ScaleUpNoOptionsAvailable
528530
klog.V(1).Infof("Max total nodes in cluster reached: %v. Current number of ready nodes: %v", a.MaxNodesTotal, len(readyNodes))
529-
} else if len(a.BypassedSchedulers) == 0 && allPodsAreNew(unschedulablePodsToHelp, currentTime) {
531+
} else if len(a.BypassedSchedulers) == 0 && !forceScaleUp && allPodsAreNew(unschedulablePodsToHelp, currentTime) {
530532
// The assumption here is that these pods have been created very recently and probably there
531533
// is more pods to come. In theory we could check the newest pod time but then if pod were created
532534
// slowly but at the pace of 1 every 2 seconds then no scale up would be triggered for long time.

cluster-autoscaler/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,8 @@ func buildAutoscaler(context ctx.Context, debuggingSnapshotter debuggingsnapshot
563563
opts.LoopStartNotifier = loopstart.NewObserversList([]loopstart.Observer{provreqProcesor})
564564

565565
podListProcessor.AddProcessor(provreqProcesor)
566+
567+
opts.Processors.ScaleUpEnforcer = provreq.NewProvisioningRequestScaleUpEnforcer()
566568
}
567569

568570
if *proactiveScaleupEnabled {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pods
18+
19+
import apiv1 "k8s.io/api/core/v1"
20+
21+
// ScaleUpEnforcer can force scale up even if all pods are new or MaxNodesTotal was achieved.
22+
type ScaleUpEnforcer interface {
23+
ShouldForceScaleUp(unschedulablePods []*apiv1.Pod) bool
24+
}
25+
26+
// NoOpScaleUpEnforcer returns false by default in case of ProvisioningRequests disabled.
27+
type NoOpScaleUpEnforcer struct {
28+
}
29+
30+
// NewDefaultScaleUpEnforcer creates an instance of ScaleUpEnforcer.
31+
func NewDefaultScaleUpEnforcer() ScaleUpEnforcer {
32+
return &NoOpScaleUpEnforcer{}
33+
}
34+
35+
// ShouldForceScaleUp returns false by default.
36+
func (p *NoOpScaleUpEnforcer) ShouldForceScaleUp(unschedulablePods []*apiv1.Pod) bool {
37+
return false
38+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pods
18+
19+
import (
20+
"testing"
21+
22+
apiv1 "k8s.io/api/core/v1"
23+
testutils "k8s.io/autoscaler/cluster-autoscaler/utils/test"
24+
)
25+
26+
func TestDefaultScaleUpEnforcer(t *testing.T) {
27+
p1 := testutils.BuildTestPod("p1", 40, 0)
28+
unschedulablePods := []*apiv1.Pod{p1}
29+
scaleUpEnforcer := NewDefaultScaleUpEnforcer()
30+
forceScaleUp := scaleUpEnforcer.ShouldForceScaleUp(unschedulablePods)
31+
if forceScaleUp {
32+
t.Errorf("Error: scaleUpEnforcer should not force scale up by default")
33+
}
34+
}

cluster-autoscaler/processors/processors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ type AutoscalingProcessors struct {
7474
ScaleStateNotifier *nodegroupchange.NodeGroupChangeObserversList
7575
// AsyncNodeGroupChecker checks if node group is upcoming or not
7676
AsyncNodeGroupStateChecker asyncnodegroups.AsyncNodeGroupStateChecker
77+
// ScaleUpEnforcer can force scale up even if all pods are new or MaxNodesTotal was achieved.
78+
ScaleUpEnforcer pods.ScaleUpEnforcer
7779
}
7880

7981
// DefaultProcessors returns default set of processors.
@@ -100,6 +102,7 @@ func DefaultProcessors(options config.AutoscalingOptions) *AutoscalingProcessors
100102
TemplateNodeInfoProvider: nodeinfosprovider.NewDefaultTemplateNodeInfoProvider(nil, false),
101103
ScaleDownCandidatesNotifier: scaledowncandidates.NewObserversList(),
102104
ScaleStateNotifier: nodegroupchange.NewNodeGroupChangeObserversList(),
105+
ScaleUpEnforcer: pods.NewDefaultScaleUpEnforcer(),
103106
}
104107
}
105108

cluster-autoscaler/processors/provreq/pods_filter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222

2323
apiv1 "k8s.io/api/core/v1"
2424
corev1 "k8s.io/api/core/v1"
25-
"k8s.io/autoscaler/cluster-autoscaler/apis/provisioningrequest/autoscaling.x-k8s.io/v1"
25+
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/provisioningrequest/autoscaling.x-k8s.io/v1"
2626
"k8s.io/autoscaler/cluster-autoscaler/context"
2727
"k8s.io/autoscaler/cluster-autoscaler/processors/pods"
2828
provreqpods "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/pods"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package provreq
18+
19+
import (
20+
apiv1 "k8s.io/api/core/v1"
21+
"k8s.io/autoscaler/cluster-autoscaler/processors/pods"
22+
)
23+
24+
// ProvisioningRequestScaleUpEnforcer forces scale up if there is any unschedulable pod that belongs to ProvisioningRequest.
25+
type ProvisioningRequestScaleUpEnforcer struct {
26+
}
27+
28+
// NewProvisioningRequestScaleUpEnforcer creates a ProvisioningRequest scale up enforcer.
29+
func NewProvisioningRequestScaleUpEnforcer() pods.ScaleUpEnforcer {
30+
return &ProvisioningRequestScaleUpEnforcer{}
31+
}
32+
33+
// ShouldForceScaleUp forces scale up if there is any unschedulable pod that belongs to ProvisioningRequest.
34+
func (p *ProvisioningRequestScaleUpEnforcer) ShouldForceScaleUp(unschedulablePods []*apiv1.Pod) bool {
35+
for _, pod := range unschedulablePods {
36+
if _, ok := provisioningRequestName(pod); ok {
37+
return true
38+
}
39+
}
40+
return false
41+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package provreq
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
apiv1 "k8s.io/api/core/v1"
24+
corev1 "k8s.io/api/core/v1"
25+
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/provisioningrequest/autoscaling.x-k8s.io/v1"
26+
"k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/pods"
27+
testutils "k8s.io/autoscaler/cluster-autoscaler/utils/test"
28+
)
29+
30+
func TestProvisioningRequestScaleUpEnforcer(t *testing.T) {
31+
prPod1 := testutils.BuildTestPod("pr-pod-1", 500, 10)
32+
prPod1.Annotations[v1.ProvisioningRequestPodAnnotationKey] = "pr-class"
33+
34+
prPod2 := testutils.BuildTestPod("pr-pod-2", 500, 10)
35+
prPod2.Annotations[pods.DeprecatedProvisioningRequestPodAnnotationKey] = "pr-class-2"
36+
37+
pod1 := testutils.BuildTestPod("pod-1", 500, 10)
38+
pod2 := testutils.BuildTestPod("pod-2", 500, 10)
39+
40+
testCases := map[string]struct {
41+
unschedulablePods []*apiv1.Pod
42+
want bool
43+
}{
44+
"Any pod with ProvisioningRequest annotation key forces scale up": {
45+
unschedulablePods: []*corev1.Pod{prPod1, pod1},
46+
want: true,
47+
},
48+
"Any pod with ProvisioningRequest deprecated annotation key forces scale up": {
49+
unschedulablePods: []*corev1.Pod{prPod2, pod1},
50+
want: true,
51+
},
52+
"Pod without ProvisioningRequest annotation key don't force scale up": {
53+
unschedulablePods: []*corev1.Pod{pod1, pod2},
54+
want: false,
55+
},
56+
"No pods don't force scale up": {
57+
unschedulablePods: []*corev1.Pod{},
58+
want: false,
59+
},
60+
}
61+
for _, test := range testCases {
62+
scaleUpEnforcer := NewProvisioningRequestScaleUpEnforcer()
63+
got := scaleUpEnforcer.ShouldForceScaleUp(test.unschedulablePods)
64+
assert.Equal(t, got, test.want)
65+
}
66+
}

cluster-autoscaler/processors/test/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"k8s.io/autoscaler/cluster-autoscaler/processors/nodegroupset"
3232
"k8s.io/autoscaler/cluster-autoscaler/processors/nodeinfosprovider"
3333
"k8s.io/autoscaler/cluster-autoscaler/processors/nodes"
34+
"k8s.io/autoscaler/cluster-autoscaler/processors/pods"
3435
"k8s.io/autoscaler/cluster-autoscaler/processors/scaledowncandidates"
3536
"k8s.io/autoscaler/cluster-autoscaler/processors/status"
3637
"k8s.io/autoscaler/cluster-autoscaler/simulator/scheduling"
@@ -56,5 +57,6 @@ func NewTestProcessors(context *context.AutoscalingContext) *processors.Autoscal
5657
ScaleDownCandidatesNotifier: scaledowncandidates.NewObserversList(),
5758
ScaleStateNotifier: nodegroupchange.NewNodeGroupChangeObserversList(),
5859
AsyncNodeGroupStateChecker: asyncnodegroups.NewDefaultAsyncNodeGroupStateChecker(),
60+
ScaleUpEnforcer: pods.NewDefaultScaleUpEnforcer(),
5961
}
6062
}

0 commit comments

Comments
 (0)