Merge pull request #138274 from wtravO/wtravo/placement-cycle-state

Add PlacementCycleState to WAS scheduler framework
This commit is contained in:
Kubernetes Prow Robot 2026-05-28 16:28:48 +05:30 committed by GitHub
commit ae84ac1a16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 384 additions and 43 deletions

View file

@ -46,6 +46,10 @@ type CycleState struct {
// or doesn't belong to any pod group.
// This field can only be non-nil when GenericWorkload feature flag is enabled.
podGroupCycleState fwk.PodGroupCycleState
// placementCycleState contains the CycleState for the current Placement being evaluated.
// If set to nil, it means this pod is not being scheduled within a placement context.
// This field can only be non-nil when GenericWorkload feature flag is enabled.
placementCycleState fwk.PlacementCycleState
}
// NewCycleState initializes a new CycleState and returns its pointer.
@ -113,6 +117,14 @@ func (c *CycleState) GetPodGroupSchedulingCycle() fwk.PodGroupCycleState {
return c.podGroupCycleState
}
func (c *CycleState) GetPlacementCycleState() fwk.PlacementCycleState {
return c.placementCycleState
}
func (c *CycleState) SetPlacementCycleState(placementCycleState fwk.PlacementCycleState) {
c.placementCycleState = placementCycleState
}
func (c *CycleState) SetSkipAllPostFilterPlugins(flag bool) {
c.skipAllPostFilterPlugins = flag
}
@ -140,6 +152,7 @@ func (c *CycleState) Clone() fwk.CycleState {
copy.skipPreBindPlugins = c.skipPreBindPlugins
copy.parallelPreBindPlugins = c.parallelPreBindPlugins
copy.podGroupCycleState = c.podGroupCycleState
copy.placementCycleState = c.placementCycleState
copy.skipAllPostFilterPlugins = c.skipAllPostFilterPlugins
return copy

View file

@ -193,3 +193,94 @@ func TestCycleStateClone(t *testing.T) {
})
}
}
func TestPlacementCycleState(t *testing.T) {
t.Run("nil by default", func(t *testing.T) {
state := NewCycleState()
if state.GetPlacementCycleState() != nil {
t.Errorf("expected nil PlacementCycleState on fresh CycleState")
}
})
t.Run("set and get", func(t *testing.T) {
state := NewCycleState()
podGroupState := NewCycleState()
placementState := NewCycleState()
placementState.SetPodGroupSchedulingCycle(podGroupState)
placementState.Write("testkey", &fakeData{data: "placementdata"})
state.SetPlacementCycleState(placementState)
got := state.GetPlacementCycleState()
if got == nil {
t.Fatal("expected non-nil PlacementCycleState after Set")
}
data, err := got.Read("testkey")
if err != nil {
t.Fatalf("unexpected error reading from PlacementCycleState: %v", err)
}
if data.(*fakeData).data != "placementdata" {
t.Errorf("expected 'placementdata', got %q", data.(*fakeData).data)
}
if got.GetPodGroupSchedulingCycle() != podGroupState {
t.Errorf("expected PlacementCycleState to expose its PodGroupCycleState")
}
})
t.Run("set to nil clears", func(t *testing.T) {
state := NewCycleState()
state.SetPlacementCycleState(NewCycleState())
state.SetPlacementCycleState(nil)
if state.GetPlacementCycleState() != nil {
t.Errorf("expected nil PlacementCycleState after setting to nil")
}
})
t.Run("clone preserves reference", func(t *testing.T) {
state := NewCycleState()
state.Write(key, &fakeData{data: "pod-data"})
placementState := NewCycleState()
placementState.Write("pkey", &fakeData{data: "placement-data"})
state.SetPlacementCycleState(placementState)
cloned := state.Clone().(*CycleState)
// The cloned state should reference the same PlacementCycleState.
if cloned.GetPlacementCycleState() == nil {
t.Fatal("cloned state should have non-nil PlacementCycleState")
}
data, err := cloned.GetPlacementCycleState().Read("pkey")
if err != nil {
t.Fatalf("unexpected error reading from cloned PlacementCycleState: %v", err)
}
if data.(*fakeData).data != "placement-data" {
t.Errorf("expected 'placement-data', got %q", data.(*fakeData).data)
}
// Writes to the PlacementCycleState via the clone should be visible from the original,
// since it's a shared reference (same as podGroupCycleState behavior).
cloned.GetPlacementCycleState().Write("newkey", &fakeData{data: "new"})
newData, err := state.GetPlacementCycleState().Read("newkey")
if err != nil {
t.Fatalf("write via clone's PlacementCycleState should be visible from original: %v", err)
}
if newData.(*fakeData).data != "new" {
t.Errorf("expected 'new', got %q", newData.(*fakeData).data)
}
})
t.Run("clone with nil placement state", func(t *testing.T) {
state := NewCycleState()
state.Write(key, &fakeData{data: "data"})
// Do not set PlacementCycleState — leave nil.
cloned := state.Clone().(*CycleState)
if cloned.GetPlacementCycleState() != nil {
t.Errorf("cloned state should have nil PlacementCycleState when original has nil")
}
})
}

View file

@ -192,7 +192,7 @@ type PlacementFeasiblePlugin interface {
// The scheduler will give up this placement and won't even evaluate remaining pods. The placement will remain eligible for preemption.
// Return Success status if the pod group can be scheduled in the current partially evaluated placement.
// After returning Success, the plugin should keep returning Success for the remaining pods.
PlacementFeasible(ctx context.Context, placementCycleState fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status
PlacementFeasible(ctx context.Context, placementCycleState fwk.PlacementCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status
}
// Framework manages the set of plugins in use by the scheduling framework.
@ -283,7 +283,7 @@ type Framework interface {
// If any plugin returns invalid status, the result will be Error and the remaining plugins won't be invoked.
// Otherwise, if at least 1 plugin returns UnschedulableAndUnresolvable, the remaining plugins won't be invoked and the result will be UnschdulableAndUnresolvable. The placement will remain eligible for preemption.
// Otherwise, if at least 1 plugin returns Unschedulable, the remaining plugins will be invoked and the result will be Unschedulable.
RunPlacementFeasiblePlugins(ctx context.Context, placementCycleState fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status
RunPlacementFeasiblePlugins(ctx context.Context, placementCycleState fwk.PlacementCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status
// AddWaitingPod creates a waiting pod instance and adds it to the framework.
// It takes the pluginsWaitTime map returned by the RunPermitPlugins.
@ -308,7 +308,8 @@ type Framework interface {
// It returns a list that stores scores from each plugin and total score for each Placement.
// It also returns *Status, which is set to non-success if any of the plugins returns
// a non-success status.
RunPlacementScorePlugins(ctx context.Context, state fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo, placements []*fwk.PodGroupAssignments) (ns []fwk.PlacementPluginScores, status *fwk.Status)
// Each PlacementCycleState is passed to ScorePlacement for the PodGroupAssignments at the same index.
RunPlacementScorePlugins(ctx context.Context, state fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo, placements []*fwk.PodGroupAssignments, placementStates []fwk.PlacementCycleState) (ns []fwk.PlacementPluginScores, status *fwk.Status)
// HasFilterPlugins returns true if at least one Filter plugin is defined.
HasFilterPlugins() bool

View file

@ -225,7 +225,7 @@ func (s *placementFeasibleState) Clone() fwk.StateData {
}
}
func getPlacementFeasibleState(placementCycleState fwk.PodGroupCycleState) *placementFeasibleState {
func getPlacementFeasibleState(placementCycleState fwk.PlacementCycleState) *placementFeasibleState {
state, err := placementCycleState.Read(placementFeasibleStateKey)
if err != nil {
state = &placementFeasibleState{}
@ -237,7 +237,7 @@ func getPlacementFeasibleState(placementCycleState fwk.PodGroupCycleState) *plac
// PlacementFeasible is responsible for enforcing the gang's MinCount constraint in the pod group scheduling cycle.
// The function will only return success once the gang's MinCount is satisfied or if the pod group is not using gang scheduling policy.
// In case there are not enough remaining pods to satisfy the gang's MinCount, it returns UnschedulableAndUnresolvable which will terminate the pod group scheduling cycle early.
func (pl *GangScheduling) PlacementFeasible(ctx context.Context, placementCycleState fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status {
func (pl *GangScheduling) PlacementFeasible(ctx context.Context, placementCycleState fwk.PlacementCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status {
pg, err := pl.podGroupLister.PodGroups(podGroupInfo.GetNamespace()).Get(podGroupInfo.GetName())
if err != nil {
return fwk.AsStatus(fmt.Errorf("failed to get podGroup %s to compute gang feasibility: %w", klog.KObj(podGroupInfo), err))

View file

@ -760,6 +760,6 @@ func (f *Fit) PlacementScoreExtensions() fwk.PlacementScoreExtensions {
}
// ScorePlacement scores capacity ratio on all nodes in the placement including all assigned pod group pods' resource requests.
func (f *Fit) ScorePlacement(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
func (f *Fit) ScorePlacement(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
return f.placementScorer.scorePlacement(ctx, podGroup, placement)
}

View file

@ -49,7 +49,7 @@ func (pl *PodGroupPodsCount) Name() string {
// Both scheduled (assumed/assigned) pods and the proposed assignments are taken into consideration
// when computing the score. This ensures that the relative difference between choices is reduced,
// and small changes to the total count result in small changes to the score.
func (pl *PodGroupPodsCount) ScorePlacement(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
func (pl *PodGroupPodsCount) ScorePlacement(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
pgState, err := pl.handle.SnapshotSharedLister().PodGroupStates().Get(podGroup.GetNamespace(), podGroup.GetName())
if err != nil {
return 0, fwk.AsStatus(err)

View file

@ -1482,13 +1482,21 @@ func (f *frameworkImpl) runScoreExtension(ctx context.Context, pl fwk.ScorePlugi
// It also returns *Status, which is set to non-success if any of the plugins returns
// a non-success status.
//
// placementStates holds the per-placement cycle state for each entry in podGroupAssignments
// (1:1 by index): placementStates[i] is the state with which podGroupAssignments[i] was
// processed, and is passed to ScorePlacement for that placement.
//
// This function mostly duplicates RunScorePlugins. Any changes to it should likely be reflected in both places.
func (f *frameworkImpl) RunPlacementScorePlugins(ctx context.Context, state fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo, podGroupAssignments []*fwk.PodGroupAssignments) (ps []fwk.PlacementPluginScores, status *fwk.Status) {
func (f *frameworkImpl) RunPlacementScorePlugins(ctx context.Context, state fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo, podGroupAssignments []*fwk.PodGroupAssignments, placementStates []fwk.PlacementCycleState) (ps []fwk.PlacementPluginScores, status *fwk.Status) {
startTime := time.Now()
defer func() {
metrics.FrameworkExtensionPointDuration.WithLabelValues(metrics.PlacementScore, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
}()
if len(podGroupAssignments) != len(placementStates) {
return nil, fwk.AsStatus(fmt.Errorf("expected one PlacementCycleState per PodGroupAssignments, got %d placement states for %d assignments", len(placementStates), len(podGroupAssignments)))
}
allPlacementPluginScores := make([]fwk.PlacementPluginScores, len(podGroupAssignments))
numPlugins := len(f.placementScorePlugins)
plugins := make([]fwk.PlacementScorePlugin, 0, numPlugins)
@ -1520,7 +1528,7 @@ func (f *frameworkImpl) RunPlacementScorePlugins(ctx context.Context, state fwk.
logger := klog.LoggerWithName(logger, pl.Name())
ctx = klog.NewContext(ctx, logger)
}
s, status := f.runPlacementScorePlugin(ctx, pl, state, podGroupInfo, pga)
s, status := f.runPlacementScorePlugin(ctx, pl, placementStates[index], podGroupInfo, pga)
if !status.IsSuccess() {
err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError())
errCh.SendWithCancel(err, cancel)
@ -1589,7 +1597,7 @@ func (f *frameworkImpl) RunPlacementScorePlugins(ctx context.Context, state fwk.
return allPlacementPluginScores, nil
}
func (f *frameworkImpl) runPlacementScorePlugin(ctx context.Context, pl fwk.PlacementScorePlugin, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
func (f *frameworkImpl) runPlacementScorePlugin(ctx context.Context, pl fwk.PlacementScorePlugin, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
if !state.ShouldRecordPluginMetrics() {
return pl.ScorePlacement(ctx, state, podGroup, placement)
}
@ -2014,7 +2022,7 @@ func (f *frameworkImpl) runPermitPlugin(ctx context.Context, pl fwk.PermitPlugin
// If any plugin returns invalid status, the result will be Error and the remaining plugins won't be invoked.
// Otherwise, if at least 1 plugin returns UnschedulableAndUnresolvable, the remaining plugins won't be invoked and the result will be UnschdulableAndUnresolvable.
// Otherwise, if at least 1 plugin returns Unschedulable, the remaining plugins will be invoked and the result will be Unschedulable.
func (f *frameworkImpl) RunPlacementFeasiblePlugins(ctx context.Context, placementCycleState fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo) (status *fwk.Status) {
func (f *frameworkImpl) RunPlacementFeasiblePlugins(ctx context.Context, placementCycleState fwk.PlacementCycleState, podGroupInfo fwk.PodGroupInfo) (status *fwk.Status) {
startTime := time.Now()
defer func() {
metrics.FrameworkExtensionPointDuration.WithLabelValues(metrics.PlacementFeasible, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
@ -2041,7 +2049,7 @@ func (f *frameworkImpl) RunPlacementFeasiblePlugins(ctx context.Context, placeme
return status
}
func (f *frameworkImpl) runPlacementFeasiblePlugin(ctx context.Context, pl framework.PlacementFeasiblePlugin, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo) *fwk.Status {
func (f *frameworkImpl) runPlacementFeasiblePlugin(ctx context.Context, pl framework.PlacementFeasiblePlugin, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo) *fwk.Status {
if !state.ShouldRecordPluginMetrics() {
return pl.PlacementFeasible(ctx, state, podGroup)
}

View file

@ -228,7 +228,7 @@ func (pl *TestPlugin) PreFilter(ctx context.Context, state fwk.CycleState, p *v1
return pl.inj.PreFilterResult, fwk.NewStatus(fwk.Code(pl.inj.PreFilterStatus), injectReason)
}
func (pl *TestPlugin) PlacementFeasible(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo) *fwk.Status {
func (pl *TestPlugin) PlacementFeasible(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo) *fwk.Status {
return fwk.NewStatus(fwk.Code(pl.inj.PlacementFeasibleStatus), injectReason)
}
@ -278,7 +278,7 @@ func (pl *TestPlugin) GeneratePlacements(ctx context.Context, state fwk.PodGroup
return &fwk.GeneratePlacementsResult{Placements: pl.inj.GeneratePlacementsResult}, fwk.NewStatus(fwk.Code(pl.inj.GeneratePlacementsStatus), injectReason)
}
func (pl *TestPlugin) ScorePlacement(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
func (pl *TestPlugin) ScorePlacement(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
return 0, fwk.NewStatus(fwk.Code(pl.inj.PlacementScoreStatus), injectReason)
}
@ -795,7 +795,7 @@ type mockPlacementFeasiblePlugin struct {
func (p *mockPlacementFeasiblePlugin) Name() string { return p.name }
func (p *mockPlacementFeasiblePlugin) PlacementFeasible(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo) *fwk.Status {
func (p *mockPlacementFeasiblePlugin) PlacementFeasible(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo) *fwk.Status {
p.called = true
return p.status
}
@ -3666,7 +3666,7 @@ func TestRecordingMetrics(t *testing.T) {
{
name: "PlacementScore - Success",
action: func(ctx context.Context, f framework.Framework) {
f.RunPlacementScorePlugins(ctx, state, nil, []*fwk.PodGroupAssignments{{Placement: &fwk.Placement{}}})
f.RunPlacementScorePlugins(ctx, state, nil, []*fwk.PodGroupAssignments{{Placement: &fwk.Placement{}}}, []fwk.PlacementCycleState{state})
},
wantExtensionPoint: "PlacementScore",
wantStatus: fwk.Success,
@ -3750,7 +3750,7 @@ func TestRecordingMetrics(t *testing.T) {
{
name: "PlacementScore - Error",
action: func(ctx context.Context, f framework.Framework) {
f.RunPlacementScorePlugins(ctx, state, nil, []*fwk.PodGroupAssignments{{Placement: &fwk.Placement{}}})
f.RunPlacementScorePlugins(ctx, state, nil, []*fwk.PodGroupAssignments{{Placement: &fwk.Placement{}}}, []fwk.PlacementCycleState{state})
},
inject: injectedResult{PlacementScoreStatus: int(fwk.Error)},
wantExtensionPoint: "PlacementScore",
@ -4440,7 +4440,7 @@ func (pl *testPlacementScorePlugin) Name() string {
return pl.name
}
func (pl *testPlacementScorePlugin) ScorePlacement(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
func (pl *testPlacementScorePlugin) ScorePlacement(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
r := pl.results[placement.Placement]
return r.score, r.status
}
@ -4682,11 +4682,13 @@ func TestRunPlacementScorePlugins(t *testing.T) {
t.Fatalf("Unexpected error during calling NewFramework, got %v", err)
}
assumedPlacements := make([]*fwk.PodGroupAssignments, len(placements))
placementStates := make([]fwk.PlacementCycleState, len(placements))
for i := range placements {
assumedPlacements[i] = &fwk.PodGroupAssignments{Placement: placements[i]}
placementStates[i] = framework.NewCycleState()
}
result, status := fw.RunPlacementScorePlugins(ctx, framework.NewCycleState(), nil, assumedPlacements)
result, status := fw.RunPlacementScorePlugins(ctx, framework.NewCycleState(), nil, assumedPlacements, placementStates)
if status.Code() != tt.wantStatusCode {
t.Errorf("got status code %s, want %s", status.Code(), tt.wantStatusCode)
}

View file

@ -129,7 +129,7 @@ type podSchedulingContext struct {
}
// initPodSchedulingContext initializes the scheduling context of a single pod for pod group scheduling cycle.
func initPodSchedulingContext(ctx context.Context, pod *v1.Pod, podGroupState *framework.CycleState, postFilterMode podGroupPostFilterMode) *podSchedulingContext {
func initPodSchedulingContext(ctx context.Context, pod *v1.Pod, placementCycleState *framework.CycleState, postFilterMode podGroupPostFilterMode) *podSchedulingContext {
logger := klog.FromContext(ctx)
// TODO(knelasevero): Remove duplicated keys from log entry calls
// When contextualized logging hits GA
@ -147,8 +147,11 @@ func initPodSchedulingContext(ctx context.Context, pod *v1.Pod, podGroupState *f
podsToActivate := framework.NewPodsToActivate()
state.Write(framework.PodsToActivateKey, podsToActivate)
podGroupCycleState := placementCycleState.GetPodGroupSchedulingCycle()
// Marks this cycle as a pod group scheduling cycle.
state.SetPodGroupSchedulingCycle(podGroupState)
state.SetPodGroupSchedulingCycle(podGroupCycleState)
// Set the placement cycle state so per-pod plugins can access placement-scoped data.
state.SetPlacementCycleState(placementCycleState)
// Skip post filters if requested.
switch postFilterMode {
@ -305,30 +308,29 @@ type podGroupAlgorithmResult struct {
// waitingOnPreemption indicates whether this pod group requires or is waiting for preemption to complete.
// This can only be set to true when the status is Unschedulable.
waitingOnPreemption bool
// placementCycleState is the state with which this placement was processed.
placementCycleState fwk.PlacementCycleState
}
// podGroupSchedulingDefaultAlgorithm runs the default algorithm for scheduling a pod group.
// It tries to schedule each pod using standard filtering and scoring logic in a fixed order.
// If a pod requires preemption to be schedulable, subsequent pods in the algorithm
// treat that pod as already scheduled on that node with victims being already removed in memory.
func (sched *Scheduler) podGroupSchedulingDefaultAlgorithm(ctx context.Context, schedFwk framework.Framework, podGroupCycleState *framework.CycleState, podGroupInfo *framework.QueuedPodGroupInfo, postFilterMode podGroupPostFilterMode) podGroupAlgorithmResult {
func (sched *Scheduler) podGroupSchedulingDefaultAlgorithm(ctx context.Context, schedFwk framework.Framework, placementCycleState *framework.CycleState, podGroupInfo *framework.QueuedPodGroupInfo, postFilterMode podGroupPostFilterMode) podGroupAlgorithmResult {
result := podGroupAlgorithmResult{
podResults: make([]algorithmResult, 0, len(podGroupInfo.QueuedPodInfos)),
status: fwk.NewStatus(fwk.Unschedulable).WithError(errPodGroupUnschedulable),
waitingOnPreemption: false,
placementCycleState: placementCycleState,
}
placementCycleState := framework.NewCycleState()
placementCycleState.SetRecordPluginMetrics(true)
placementCycleState.SetPodGroupSchedulingCycle(podGroupCycleState)
logger := klog.FromContext(ctx)
logger.V(5).Info("Running a pod group scheduling algorithm", "podGroup", klog.KObj(podGroupInfo), "unscheduledPodsCount", len(podGroupInfo.QueuedPodInfos))
requiresPreemption := false
anyScheduled := false
for _, podInfo := range podGroupInfo.QueuedPodInfos {
podResult, revertFn := sched.podGroupPodSchedulingAlgorithm(ctx, schedFwk, podGroupCycleState, podGroupInfo, podInfo, postFilterMode)
podResult, revertFn := sched.podGroupPodSchedulingAlgorithm(ctx, schedFwk, placementCycleState, podGroupInfo, podInfo, postFilterMode)
result.podResults = append(result.podResults, podResult)
if revertFn != nil {
// We unreserve the pod at the end of the whole algorithm (via defer) because it should be ultimately returned to the queue,
@ -382,9 +384,9 @@ func (sched *Scheduler) podGroupSchedulingDefaultAlgorithm(ctx context.Context,
// podGroupPodSchedulingAlgorithm runs a scheduling algorithm for individual pod from a pod group.
// It returns the algorithm result together with the revert function.
func (sched *Scheduler) podGroupPodSchedulingAlgorithm(ctx context.Context, schedFwk framework.Framework, podGroupCycleState *framework.CycleState, podGroupInfo *framework.QueuedPodGroupInfo, podInfo *framework.QueuedPodInfo, postFilterMode podGroupPostFilterMode) (algorithmResult, func()) {
func (sched *Scheduler) podGroupPodSchedulingAlgorithm(ctx context.Context, schedFwk framework.Framework, placementCycleState *framework.CycleState, podGroupInfo *framework.QueuedPodGroupInfo, podInfo *framework.QueuedPodInfo, postFilterMode podGroupPostFilterMode) (algorithmResult, func()) {
pod := podInfo.Pod
podCtx := initPodSchedulingContext(ctx, pod, podGroupCycleState, postFilterMode)
podCtx := initPodSchedulingContext(ctx, pod, placementCycleState, postFilterMode)
logger := podCtx.logger
ctx = klog.NewContext(ctx, logger)
start := time.Now()
@ -451,9 +453,11 @@ func completePodGroupAlgorithmResult(ctx context.Context, podGroupInfo *framewor
copy(newResults, podGroupResult.podResults)
for i := numInResult; i < numInQueue; i++ {
pInfo := podGroupInfo.QueuedPodInfos[i]
placementCycleState := framework.NewCycleState()
placementCycleState.SetPodGroupSchedulingCycle(podGroupState)
newResults[i] = algorithmResult{
pod: pInfo.Pod,
podCtx: initPodSchedulingContext(ctx, pInfo.Pod, podGroupState, postFilterMode),
podCtx: initPodSchedulingContext(ctx, pInfo.Pod, placementCycleState, postFilterMode),
status: podGroupResult.status.Clone(),
}
}
@ -656,7 +660,9 @@ func (sched *Scheduler) podGroupSchedulingPlacementAlgorithm(ctx context.Context
status: fwk.AsStatus(fmt.Errorf("failed to assume pod group placement: %w", err)),
}
}
result := sched.podGroupSchedulingDefaultAlgorithm(ctx, schedFwk, podGroupCycleState, podGroupInfo, postFilterMode)
placementCycleState := framework.NewCycleState()
placementCycleState.SetPodGroupSchedulingCycle(podGroupCycleState)
result := sched.podGroupSchedulingDefaultAlgorithm(ctx, schedFwk, placementCycleState, podGroupInfo, postFilterMode)
sched.nodeInfoSnapshot.ForgetPlacement()
if result.status.IsError() {
return result
@ -696,9 +702,9 @@ func (sched *Scheduler) podGroupSchedulingPlacementAlgorithm(ctx context.Context
// findBestPlacement uses PlacementScore plugins to determine the best placement based on the scheduling results.
func (sched *Scheduler) findBestPlacement(ctx context.Context, schedFwk framework.Framework, podGroupCycleState fwk.PodGroupCycleState, podGroupInfo *framework.QueuedPodGroupInfo, successfulResults map[*fwk.Placement]*podGroupAlgorithmResult) (*fwk.Placement, *fwk.Status) {
placementPodGroupAssignments := makePodGroupAssignments(successfulResults)
placementPodGroupAssignments, placementStates := makePodGroupAssignments(successfulResults)
scores, status := schedFwk.RunPlacementScorePlugins(ctx, podGroupCycleState, podGroupInfo, placementPodGroupAssignments)
scores, status := schedFwk.RunPlacementScorePlugins(ctx, podGroupCycleState, podGroupInfo, placementPodGroupAssignments, placementStates)
if !status.IsSuccess() {
return nil, status
}
@ -728,16 +734,18 @@ func (sched *Scheduler) findBestPlacement(ctx context.Context, schedFwk framewor
return bestScore.Placement, nil
}
func makePodGroupAssignments(successfulResults map[*fwk.Placement]*podGroupAlgorithmResult) []*fwk.PodGroupAssignments {
func makePodGroupAssignments(successfulResults map[*fwk.Placement]*podGroupAlgorithmResult) ([]*fwk.PodGroupAssignments, []fwk.PlacementCycleState) {
placementPodGroupAssignments := make([]*fwk.PodGroupAssignments, 0, len(successfulResults))
placementStates := make([]fwk.PlacementCycleState, 0, len(successfulResults))
for placement, result := range successfulResults {
proposedAssignments := makeProposedAssignments(result)
placementPodGroupAssignments = append(placementPodGroupAssignments, &fwk.PodGroupAssignments{
Placement: placement,
ProposedAssignments: proposedAssignments,
})
placementStates = append(placementStates, result.placementCycleState)
}
return placementPodGroupAssignments
return placementPodGroupAssignments, placementStates
}
// makeProposedAssignments builds a list of proposedAssignments from the result of a pod group scheduling attempt.
@ -759,5 +767,10 @@ func (sched *Scheduler) podGroupSchedulingAlgorithm(ctx context.Context, schedFw
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) {
return sched.podGroupSchedulingPlacementAlgorithm(podGroupCycleCtx, schedFwk, podGroupCycleState, podGroupInfo, postFilterMode)
}
return sched.podGroupSchedulingDefaultAlgorithm(podGroupCycleCtx, schedFwk, podGroupCycleState, podGroupInfo, postFilterMode)
// The non-TAS default algorithm does not evaluate placement candidates, but it
// still runs in a single implicit placement context so placement-scoped
// extension points can use the same state plumbing as TAS.
placementCycleState := framework.NewCycleState()
placementCycleState.SetPodGroupSchedulingCycle(podGroupCycleState)
return sched.podGroupSchedulingDefaultAlgorithm(podGroupCycleCtx, schedFwk, placementCycleState, podGroupInfo, postFilterMode)
}

View file

@ -139,7 +139,7 @@ func (mp *fakePlacementFeasiblePlugin) Name() string {
// - The outer slice represents distinct placements (e.g., when evaluating multiple topology placements).
// - The inner slice represents the pod-by-pod evaluation within a single placement.
// It uses placementCycleState to track how many pods have been evaluated in the current placement.
func (mp *fakePlacementFeasiblePlugin) PlacementFeasible(ctx context.Context, placementCycleState fwk.PodGroupCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status {
func (mp *fakePlacementFeasiblePlugin) PlacementFeasible(ctx context.Context, placementCycleState fwk.PlacementCycleState, podGroupInfo fwk.PodGroupInfo) *fwk.Status {
// If no mock statuses are configured, always succeed.
if len(mp.placementFeasibleStatuses) == 0 {
return nil
@ -1642,7 +1642,9 @@ func TestSubmitPodGroupAlgorithmResult(t *testing.T) {
for i := range tt.algorithmResult.podResults {
pod := podGroupInfo.QueuedPodInfos[i].Pod
podCtx := initPodSchedulingContext(ctx, pod, podGroupCycleState, runAllPostFilters)
placementCycleState := framework.NewCycleState()
placementCycleState.SetPodGroupSchedulingCycle(podGroupCycleState)
podCtx := initPodSchedulingContext(ctx, pod, placementCycleState, runAllPostFilters)
tt.algorithmResult.podResults[i].podCtx = podCtx
}
@ -2091,7 +2093,7 @@ func (mp *fakePlacementPlugin) PlacementScoreExtensions() fwk.PlacementScoreExte
return nil
}
func (mp *fakePlacementPlugin) ScorePlacement(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
func (mp *fakePlacementPlugin) ScorePlacement(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
return mp.scorePlacementsResult[placement.Name], mp.scorePlacementsStatus[placement.Name]
}
@ -2428,6 +2430,7 @@ func TestPodGroupSchedulingPlacementAlgorithm(t *testing.T) {
algorithmResult{},
ScheduleResult{},
fwk.Status{}),
cmpopts.IgnoreFields(podGroupAlgorithmResult{}, "placementCycleState"),
cmpopts.IgnoreFields(algorithmResult{}, "podCtx", "schedulingDuration"),
statusCmpOpt,
}
@ -2593,6 +2596,182 @@ func TestPodGroupSchedulingPlacementAlgorithm_Scoring(t *testing.T) {
}
}
// placementStateTracker is a fake plugin that writes to PlacementCycleState during Filter
// and reads from it during ScorePlacement, to verify the lifecycle of PlacementCycleState.
type placementStateTracker struct {
name string
mu sync.Mutex
// scoreReadValues records what value was read from PlacementCycleState
// during each ScorePlacement call, keyed by placement name.
scoreReadValues map[string]string
// generatePlacementsResult defines the placements to generate.
generatePlacementsResult map[string][]string
}
type placementStateData struct {
value string
}
func (d *placementStateData) Clone() fwk.StateData { return d }
var placementStateKey fwk.StateKey = "placementStateTracker"
var _ fwk.FilterPlugin = &placementStateTracker{}
var _ fwk.PlacementGeneratePlugin = &placementStateTracker{}
var _ fwk.PlacementScorePlugin = &placementStateTracker{}
func (p *placementStateTracker) Name() string { return p.name }
func (p *placementStateTracker) Filter(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
placementState := state.GetPlacementCycleState()
if placementState == nil {
return fwk.NewStatus(fwk.Error, "PlacementCycleState is nil during Filter")
}
// Write the node name as a marker so ScorePlacement can verify
// which placement's state it received.
placementState.Write(placementStateKey, &placementStateData{value: nodeInfo.Node().Name})
return nil
}
func (p *placementStateTracker) ScorePlacement(ctx context.Context, state fwk.PlacementCycleState, podGroup fwk.PodGroupInfo, placement *fwk.PodGroupAssignments) (int64, *fwk.Status) {
if state == nil {
return 0, fwk.NewStatus(fwk.Error, "PlacementCycleState is nil during ScorePlacement")
}
data, err := state.Read(placementStateKey)
if err != nil {
return 0, fwk.NewStatus(fwk.Error, fmt.Sprintf("failed to read PlacementCycleState for %s: %v", placement.Name, err))
}
p.mu.Lock()
defer p.mu.Unlock()
p.scoreReadValues[placement.Name] = data.(*placementStateData).value
return 1, nil
}
func (p *placementStateTracker) PlacementScoreExtensions() fwk.PlacementScoreExtensions {
return nil
}
func (p *placementStateTracker) GeneratePlacements(ctx context.Context, state fwk.PodGroupCycleState, podGroup fwk.PodGroupInfo, parentPlacement *fwk.Placement) (*fwk.GeneratePlacementsResult, *fwk.Status) {
parentNodes := map[string]fwk.NodeInfo{}
for _, node := range parentPlacement.Nodes {
parentNodes[node.Node().Name] = node
}
resultPlacements := make([]*fwk.Placement, 0, len(p.generatePlacementsResult))
for placementName, nodeNames := range p.generatePlacementsResult {
placement := &fwk.Placement{Name: placementName}
for _, nodeName := range nodeNames {
placement.Nodes = append(placement.Nodes, parentNodes[nodeName])
}
resultPlacements = append(resultPlacements, placement)
}
return &fwk.GeneratePlacementsResult{Placements: resultPlacements}, nil
}
func TestPlacementCycleStateLifecycle(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.TopologyAwareWorkloadScheduling: true,
features.GenericWorkload: true,
features.GangScheduling: true,
})
// A single scenario exercises both isolation and continuity:
// - Filter writes a node-name marker into PlacementCycleState during each placement's simulation.
// - ScorePlacement reads from the placement state after all simulations.
// Assertions verify:
// 1. Each placement's scorer reads only the value its own simulation wrote (isolation).
// 2. Data written during each placement's simulation remains readable during its scoring (continuity from simulation to scoring).
nodes := []*v1.Node{
st.MakeNode().Name("node1").Obj(),
st.MakeNode().Name("node2").Obj(),
}
podGroupPod := st.MakePod().Name("foo").UID("foo").PodGroupName("pg").Obj()
logger, ctx := ktesting.NewTestContext(t)
informerFactory := informers.NewSharedInformerFactory(clientsetfake.NewClientset(), 0)
queue := internalqueue.NewSchedulingQueue(nil, informerFactory)
tracker := &placementStateTracker{
name: "StateTracker",
scoreReadValues: make(map[string]string),
generatePlacementsResult: map[string][]string{
"placementA": {nodes[0].Name},
"placementB": {nodes[1].Name},
},
}
registry := []tf.RegisterPluginFunc{
tf.RegisterPlacementGeneratePlugin(tracker.Name(), func(_ context.Context, _ runtime.Object, _ fwk.Handle) (fwk.Plugin, error) {
return tracker, nil
}),
tf.RegisterPlacementScorePlugin(tracker.Name(), func(_ context.Context, _ runtime.Object, _ fwk.Handle) (fwk.Plugin, error) {
return tracker, nil
}, 1),
tf.RegisterFilterPlugin(tracker.Name(), func(_ context.Context, _ runtime.Object, _ fwk.Handle) (fwk.Plugin, error) {
return tracker, nil
}),
}
snapshot := internalcache.NewEmptySnapshot()
schedFwk, err := tf.NewFramework(ctx,
append(registry,
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
),
"test-scheduler",
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithPodNominator(queue),
)
if err != nil {
t.Fatalf("Failed to create framework: %v", err)
}
cache := internalcache.New(ctx, nil, true)
for _, node := range nodes {
cache.AddNode(logger, node)
}
sched := &Scheduler{
Cache: cache,
nodeInfoSnapshot: snapshot,
SchedulingQueue: queue,
Profiles: profile.Map{"test-scheduler": schedFwk},
}
sched.SchedulePod = sched.schedulePod
if err := sched.Cache.UpdateSnapshot(logger, sched.nodeInfoSnapshot); err != nil {
t.Fatalf("Failed to update snapshot: %v", err)
}
pgInfo := &framework.QueuedPodGroupInfo{
QueuedPodInfos: []*framework.QueuedPodInfo{
{PodInfo: &framework.PodInfo{Pod: podGroupPod}},
},
PodGroupInfo: &framework.PodGroupInfo{
UnscheduledPods: []*v1.Pod{podGroupPod},
},
}
result := sched.podGroupSchedulingPlacementAlgorithm(ctx, schedFwk, framework.NewCycleState(), pgInfo, runAllPostFilters)
if !result.status.IsSuccess() {
t.Fatalf("Expected success, got: %v", result.status)
}
// Each placement's scorer must read only what its own simulation wrote
// (placementA simulated on node1, placementB on node2). This proves both:
// - Continuity: data written during a placement's simulation is readable during its scoring.
// - Isolation: a placement's scorer does not see another placement's writes.
expectedScoreReadValues := map[string]string{"placementA": "node1", "placementB": "node2"}
if diff := cmp.Diff(expectedScoreReadValues, tracker.scoreReadValues); diff != "" {
t.Errorf("Unexpected scoreReadValues (-want,+got)\n%s", diff)
}
}
type fakeDefaultPreemption struct {
*fakePodGroupPlugin
}

View file

@ -96,14 +96,47 @@ type CycleState interface {
// IsPodGroupSchedulingCycle returns true if this cycle is a pod group scheduling cycle.
// If set to false, it means that the pod referencing this CycleState either passed the pod group cycle
// or doesn't belong to any pod group.
// This field can only be set to true when GenericWorkload feature flag is enabled.
// This field can only be true when GenericWorkload feature flag is enabled.
IsPodGroupSchedulingCycle() bool
// GetPodGroupSchedulingCycle gets the cycle state of the PodGroup for a Pod.
// This should be only used when GenericWorkload feature flag is enabled.
GetPodGroupSchedulingCycle() PodGroupCycleState
// SetPodGroupSchedulingCycle sets the cycle state of the PodGroup for a Pod.
// GetPlacementCycleState gets the cycle state of the current Placement for a Pod.
// Returns nil if this pod is not being scheduled within a placement context.
// This should be only used when GenericWorkload feature flag is enabled.
SetPodGroupSchedulingCycle(PodGroupCycleState)
GetPlacementCycleState() PlacementCycleState
// SetPlacementCycleState sets the cycle state of the current Placement for a Pod.
// This should be only used when GenericWorkload feature flag is enabled.
SetPlacementCycleState(PlacementCycleState)
}
// PlacementCycleState provides a mechanism for plugins to store and retrieve arbitrary data
// scoped to a single placement candidate within a pod group scheduling cycle.
// Data stored in PlacementCycleState is shared across all pods scheduled within the same
// placement iteration and is passed to ScorePlacement for that placement.
// PlacementCycleState does not provide any data protection, as all plugins are assumed to be
// trusted.
type PlacementCycleState interface {
// ShouldRecordPluginMetrics returns whether metrics.PluginExecutionDuration metrics
// should be recorded.
// This function is mostly for the scheduling framework runtime, plugins usually don't have to use it.
ShouldRecordPluginMetrics() bool
// Read retrieves data with the given "key" from PlacementCycleState. If the key is not
// present, ErrNotFound is returned.
//
// See PlacementCycleState for notes on concurrency.
Read(key StateKey) (StateData, error)
// Write stores the given "val" in PlacementCycleState with the given "key".
//
// See PlacementCycleState for notes on concurrency.
Write(key StateKey, val StateData)
// Delete deletes data with the given key from PlacementCycleState.
//
// See PlacementCycleState for notes on concurrency.
Delete(key StateKey)
// GetPodGroupSchedulingCycle gets the cycle state of the PodGroup for this Placement.
// This should be only used when GenericWorkload feature flag is enabled.
GetPodGroupSchedulingCycle() PodGroupCycleState
}
// PodGroupCycleState provides a mechanism for plugins that operate on pod groups to store and retrieve arbitrary data.

View file

@ -795,9 +795,10 @@ type PlacementScorePlugin interface {
// ScorePlacement calculates a score for a given Placement.
// This function is called only for Placements that have been deemed feasible for the sufficient number of pods in the PodGroup scheduling cycle.
// The PodGroupAssignments indicates the node assigned to each pod within this Placement.
// The state is scoped to the Placement being scored and can be used to access the owning PodGroup cycle state.
// The returned score is a int64 with higher scores generally indicating more preferable Placements.
// Plugins can implement various scoring strategies, such as bin packing to minimize resource fragmentation.
ScorePlacement(ctx context.Context, state PodGroupCycleState, podGroup PodGroupInfo, placement *PodGroupAssignments) (int64, *Status)
ScorePlacement(ctx context.Context, state PlacementCycleState, podGroup PodGroupInfo, placement *PodGroupAssignments) (int64, *Status)
// PlacementScoreExtensions returns a PlacementScoreExtensions interface if it implements one, or nil if does not.
PlacementScoreExtensions() PlacementScoreExtensions