@@ -115,6 +115,10 @@ type StatefulSetScheduler struct {
115115 // replicas is the (cached) number of statefulset replicas.
116116 replicas int32
117117
118+ // minReplicas is the minimum number of replicas for the statefulset.
119+ // When set, the scheduler spreads vreplicas across at least this many pods.
120+ minReplicas int32
121+
118122 // isLeader signals whether a given Scheduler instance is leader or not.
119123 // The autoscaler is considered the leader when ephemeralLeaderElectionObject is in a
120124 // bucket where we've been promoted.
@@ -225,6 +229,7 @@ func newStatefulSetScheduler(ctx context.Context,
225229 stateAccessor : stateAccessor ,
226230 reserved : make (map [types.NamespacedName ]map [string ]int32 ),
227231 autoscaler : autoscaler ,
232+ minReplicas : cfg .MinReplicas ,
228233 }
229234
230235 // Monitor our statefulset
@@ -377,34 +382,50 @@ func (s *StatefulSetScheduler) scheduleVPod(ctx context.Context, vpod scheduler.
377382 // - allocates as many vreplicas as possible to the same pod(s)
378383 // - allocates remaining vreplicas to new pods
379384
385+ // When minReplicas is set, ensure vreplicas are spread across at least
386+ // that many pods by inflating the target to max(requested, minReplicas).
387+ // Cap at schedulable pods to avoid requeueing when pods aren't ready yet.
388+ target := vpod .GetVReplicas ()
389+ if s .minReplicas > target {
390+ schedulable := int32 (len (state .SchedulablePods )) //nolint:gosec // G115: len is bounded by cluster size, cannot overflow int32
391+ if s .minReplicas < schedulable {
392+ target = s .minReplicas
393+ } else {
394+ target = schedulable
395+ }
396+ if target < vpod .GetVReplicas () {
397+ target = vpod .GetVReplicas ()
398+ }
399+ }
400+
380401 // Exact number of vreplicas => do nothing
381402 tr := scheduler .GetTotalVReplicas (placements )
382- if tr == vpod . GetVReplicas () {
403+ if tr == target {
383404 logger .Debug ("scheduling succeeded (already scheduled)" )
384405
385406 // Fully placed. Nothing to do
386407 return placements , nil
387408 }
388409
389410 // Need less => scale down
390- if tr > vpod . GetVReplicas () {
391- logger .Debugw ("scaling down" , zap .Int32 ("vreplicas" , tr ), zap .Int32 ("new vreplicas" , vpod . GetVReplicas () ),
411+ if tr > target {
412+ logger .Debugw ("scaling down" , zap .Int32 ("vreplicas" , tr ), zap .Int32 ("new vreplicas" , target ),
392413 zap .Any ("placements" , placements ),
393414 zap .Any ("existingPlacements" , existingPlacements ))
394415
395- placements = s .removeReplicas (tr - vpod . GetVReplicas () , placements )
416+ placements = s .removeReplicas (tr - target , placements )
396417
397418 // Do not trigger the autoscaler to avoid unnecessary churn
398419
399420 return placements , nil
400421 }
401422
402423 // Need more => scale up
403- logger .Debugw ("scaling up" , zap .Int32 ("vreplicas" , tr ), zap .Int32 ("new vreplicas" , vpod . GetVReplicas () ),
424+ logger .Debugw ("scaling up" , zap .Int32 ("vreplicas" , tr ), zap .Int32 ("new vreplicas" , target ),
404425 zap .Any ("placements" , placements ),
405426 zap .Any ("existingPlacements" , existingPlacements ))
406427
407- placements , left := s .addReplicas (state , reservedByPodName , vpod , vpod . GetVReplicas () - tr , placements )
428+ placements , left := s .addReplicas (state , reservedByPodName , vpod , target - tr , placements )
408429
409430 if left > 0 {
410431 // Give time for the autoscaler to do its job
0 commit comments