Skip to content

Commit fbaacd5

Browse files
authored
fix(scheduler): backport nil-scenario preempt guard to v0.15 (#1699)
Signed-off-by: Erez Freiberger <enoodle@gmail.com>
1 parent 90431e2 commit fbaacd5

5 files changed

Lines changed: 32 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
99
### Fixed
1010
- Fixed default node-scale-adjuster image name (`node-scale-adjuster``nodescaleadjuster`) so it matches the image published to GHCR
1111
- Account for native sidecar containers (initContainers with `restartPolicy: Always`, KEP-753) in pod resource accounting, matching kubelet's `AggregateContainerRequests`. Previously, native sidecar requests were max'd against regular containers instead of summed with them, causing the scheduler to bind pods that kubelet then rejected at admission with `OutOfCpu`/`OutOfGpu`. [#1556](https://github.com/kai-scheduler/KAI-Scheduler/pull/1556)
12+
- Fixed scheduler nil-pointer panic in the preempt scenario builder when a (partial) job has no tasks to allocate (`NewIdleGpusFilter` dereferenced a nil scenario); added the missing nil-guard matching the sibling filters [#1664](https://github.com/kai-scheduler/KAI-Scheduler/issues/1664) [sam-huang1223](https://github.com/sam-huang1223)
1213

1314
## [v0.15.2] - 2026-06-10
1415

@@ -22,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2223

2324
### Changed
2425
- Updated Go toolchain and base build images to v1.26.3.
26+
- Fixed Helm chart not wiring `podgrouper.queueLabelKey` into `spec.global.queueLabelKey` on the Config CR, so custom queue label keys were ignored at install time [#1655](https://github.com/kai-scheduler/KAI-Scheduler/pull/1655) [dttung2905](https://github.com/dttung2905)
2527

2628
## [v0.15.0] - 2026-05-20
2729

pkg/scheduler/actions/common/solvers/accumulated_scenario_filters/idle_gpus/idle_gpus.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type AccumulatedIdleGpus struct {
5353

5454
func NewIdleGpusFilter(
5555
scenario *scenario.ByNodeScenario, nodeInfosMap map[string]*node_info.NodeInfo) *AccumulatedIdleGpus {
56+
if scenario == nil {
57+
return nil
58+
}
5659
idleGpusMap, relevantNodesSorted := createGpuMap(nodeInfosMap, len(scenario.PendingTasks()))
5760

5861
filter := &AccumulatedIdleGpus{

pkg/scheduler/actions/common/solvers/accumulated_scenario_filters/idle_gpus/idle_gpus_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,3 +1378,9 @@ func TestAccumulatedIdleGpus_Filter(t *testing.T) {
13781378
})
13791379
}
13801380
}
1381+
1382+
func Test_NewIdleGpusFilter_NilScenario(t *testing.T) {
1383+
if filter := NewIdleGpusFilter(nil, map[string]*node_info.NodeInfo{}); filter != nil {
1384+
t.Fatalf("expected nil filter for nil scenario, got %#v", filter)
1385+
}
1386+
}

pkg/scheduler/actions/common/solvers/pod_scenario_builder.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ func (asb *PodAccumulatedScenarioBuilder) GetNextScenario() *solverscenario.ByNo
115115
// outer state. advanceFirst controls whether the first pass starts by popping a
116116
// victim or by evaluating the current state as-is.
117117
func (asb *PodAccumulatedScenarioBuilder) iterate(advanceFirst bool) *solverscenario.ByNodeScenario {
118+
if asb.lastScenario == nil {
119+
return nil
120+
}
118121
needAdvance := advanceFirst
119122
for {
120123
if sub := asb.nextFromSubEmitter(); sub != nil {

pkg/scheduler/actions/common/solvers/pod_scenario_builder_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,24 @@ var _ = Describe("PodAccumulatedScenarioBuilder", func() {
284284
Expect(numberOfGeneratedScenarios).To(Equal(len(potentialVictimsPerScenario)))
285285
})
286286
})
287+
288+
Context("preemptor with no tasks to allocate - nil scenario", func() {
289+
BeforeEach(func() {
290+
ssn, _ = initializeSession(0, 0)
291+
submitQueue := createQueue("team-a")
292+
ssn.ClusterInfo.Queues[submitQueue.UID] = submitQueue
293+
// Running tasks are not allocatable, so the job has no tasks to allocate.
294+
reclaimerJob, _ = createJobWithTasks(1, 1, "team-a", v1.PodRunning, []v1.ResourceRequirements{})
295+
recordedVictimsJobs := []*podgroup_info.PodGroupInfo{}
296+
victimsQueue := utils.GetVictimsQueue(ssn, nil)
297+
298+
scenarioBuilder = NewPodAccumulatedScenarioBuilder(ssn, reclaimerJob, recordedVictimsJobs, victimsQueue, ssn.ClusterInfo.Nodes)
299+
})
300+
It("does not panic and yields no scenario", func() {
301+
Expect(scenarioBuilder.GetValidScenario()).To(BeNil())
302+
Expect(scenarioBuilder.GetNextScenario()).To(BeNil())
303+
})
304+
})
287305
})
288306

289307
func initializeSession(jobsCount, tasksPerJob int) (*framework.Session, []*pod_info.PodInfo) {

0 commit comments

Comments
 (0)